Merge "Remove StaticShadowHelper API abstractions." into pi-preview1-androidx-dev
diff --git a/app-toolkit/build.gradle b/app-toolkit/build.gradle
index 9bcb827..c771ed8 100644
--- a/app-toolkit/build.gradle
+++ b/app-toolkit/build.gradle
@@ -26,3 +26,5 @@
classpath build_libs.kotlin.gradle_plugin
}
}
+
+apply from: 'buildSrc/jetify.gradle'
diff --git a/app-toolkit/common/api/0.0.0.txt b/app-toolkit/common/api_legacy/0.0.0.txt
similarity index 100%
rename from app-toolkit/common/api/0.0.0.txt
rename to app-toolkit/common/api_legacy/0.0.0.txt
diff --git a/app-toolkit/common/api/1.0.0.txt b/app-toolkit/common/api_legacy/1.0.0.txt
similarity index 100%
rename from app-toolkit/common/api/1.0.0.txt
rename to app-toolkit/common/api_legacy/1.0.0.txt
diff --git a/app-toolkit/common/api/1.1.0.txt b/app-toolkit/common/api_legacy/1.1.0.txt
similarity index 100%
rename from app-toolkit/common/api/1.1.0.txt
rename to app-toolkit/common/api_legacy/1.1.0.txt
diff --git a/app-toolkit/common/src/main/java/android/arch/core/internal/FastSafeIterableMap.java b/app-toolkit/common/src/main/java/android/arch/core/internal/FastSafeIterableMap.java
deleted file mode 100644
index dbd4d5f..0000000
--- a/app-toolkit/common/src/main/java/android/arch/core/internal/FastSafeIterableMap.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.core.internal;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Poor's man LinkedHashMap, which supports modifications during iterations.
- * Takes more memory that {@link SafeIterableMap}
- * It is NOT thread safe.
- *
- * @param <K> Key type
- * @param <V> Value type
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FastSafeIterableMap<K, V> extends SafeIterableMap<K, V> {
-
- private HashMap<K, Entry<K, V>> mHashMap = new HashMap<>();
-
- @Override
- protected Entry<K, V> get(K k) {
- return mHashMap.get(k);
- }
-
- @Override
- public V putIfAbsent(@NonNull K key, @NonNull V v) {
- Entry<K, V> current = get(key);
- if (current != null) {
- return current.mValue;
- }
- mHashMap.put(key, put(key, v));
- return null;
- }
-
- @Override
- public V remove(@NonNull K key) {
- V removed = super.remove(key);
- mHashMap.remove(key);
- return removed;
- }
-
- /**
- * Returns {@code true} if this map contains a mapping for the specified
- * key.
- */
- public boolean contains(K key) {
- return mHashMap.containsKey(key);
- }
-
- /**
- * Return an entry added to prior to an entry associated with the given key.
- *
- * @param k the key
- */
- public Map.Entry<K, V> ceil(K k) {
- if (contains(k)) {
- return mHashMap.get(k).mPrevious;
- }
- return null;
- }
-}
diff --git a/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java b/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java
deleted file mode 100644
index 00e102f..0000000
--- a/app-toolkit/common/src/main/java/android/arch/core/internal/SafeIterableMap.java
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
- * 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.core.internal;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-
-import java.util.Iterator;
-import java.util.Map;
-import java.util.WeakHashMap;
-
-/**
- * LinkedList, which pretends to be a map and supports modifications during iterations.
- * It is NOT thread safe.
- *
- * @param <K> Key type
- * @param <V> Value type
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SafeIterableMap<K, V> implements Iterable<Map.Entry<K, V>> {
-
- private Entry<K, V> mStart;
- private Entry<K, V> mEnd;
- // using WeakHashMap over List<WeakReference>, so we don't have to manually remove
- // WeakReferences that have null in them.
- private WeakHashMap<SupportRemove<K, V>, Boolean> mIterators = new WeakHashMap<>();
- private int mSize = 0;
-
- protected Entry<K, V> get(K k) {
- Entry<K, V> currentNode = mStart;
- while (currentNode != null) {
- if (currentNode.mKey.equals(k)) {
- break;
- }
- currentNode = currentNode.mNext;
- }
- return currentNode;
- }
-
- /**
- * If the specified key is not already associated
- * with a value, associates it with the given value.
- *
- * @param key key with which the specified value is to be associated
- * @param v value to be associated with the specified key
- * @return the previous value associated with the specified key,
- * or {@code null} if there was no mapping for the key
- */
- public V putIfAbsent(@NonNull K key, @NonNull V v) {
- Entry<K, V> entry = get(key);
- if (entry != null) {
- return entry.mValue;
- }
- put(key, v);
- return null;
- }
-
- protected Entry<K, V> put(@NonNull K key, @NonNull V v) {
- Entry<K, V> newEntry = new Entry<>(key, v);
- mSize++;
- if (mEnd == null) {
- mStart = newEntry;
- mEnd = mStart;
- return newEntry;
- }
-
- mEnd.mNext = newEntry;
- newEntry.mPrevious = mEnd;
- mEnd = newEntry;
- return newEntry;
-
- }
-
- /**
- * Removes the mapping for a key from this map if it is present.
- *
- * @param key key whose mapping is to be removed from the map
- * @return the previous value associated with the specified key,
- * or {@code null} if there was no mapping for the key
- */
- public V remove(@NonNull K key) {
- Entry<K, V> toRemove = get(key);
- if (toRemove == null) {
- return null;
- }
- mSize--;
- if (!mIterators.isEmpty()) {
- for (SupportRemove<K, V> iter : mIterators.keySet()) {
- iter.supportRemove(toRemove);
- }
- }
-
- if (toRemove.mPrevious != null) {
- toRemove.mPrevious.mNext = toRemove.mNext;
- } else {
- mStart = toRemove.mNext;
- }
-
- if (toRemove.mNext != null) {
- toRemove.mNext.mPrevious = toRemove.mPrevious;
- } else {
- mEnd = toRemove.mPrevious;
- }
-
- toRemove.mNext = null;
- toRemove.mPrevious = null;
- return toRemove.mValue;
- }
-
- /**
- * @return the number of elements in this map
- */
- public int size() {
- return mSize;
- }
-
- /**
- * @return an ascending iterator, which doesn't include new elements added during an
- * iteration.
- */
- @NonNull
- @Override
- public Iterator<Map.Entry<K, V>> iterator() {
- ListIterator<K, V> iterator = new AscendingIterator<>(mStart, mEnd);
- mIterators.put(iterator, false);
- return iterator;
- }
-
- /**
- * @return an descending iterator, which doesn't include new elements added during an
- * iteration.
- */
- public Iterator<Map.Entry<K, V>> descendingIterator() {
- DescendingIterator<K, V> iterator = new DescendingIterator<>(mEnd, mStart);
- mIterators.put(iterator, false);
- return iterator;
- }
-
- /**
- * return an iterator with additions.
- */
- public IteratorWithAdditions iteratorWithAdditions() {
- @SuppressWarnings("unchecked")
- IteratorWithAdditions iterator = new IteratorWithAdditions();
- mIterators.put(iterator, false);
- return iterator;
- }
-
- /**
- * @return eldest added entry or null
- */
- public Map.Entry<K, V> eldest() {
- return mStart;
- }
-
- /**
- * @return newest added entry or null
- */
- public Map.Entry<K, V> newest() {
- return mEnd;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof SafeIterableMap)) {
- return false;
- }
- SafeIterableMap map = (SafeIterableMap) obj;
- if (this.size() != map.size()) {
- return false;
- }
- Iterator<Map.Entry<K, V>> iterator1 = iterator();
- Iterator iterator2 = map.iterator();
- while (iterator1.hasNext() && iterator2.hasNext()) {
- Map.Entry<K, V> next1 = iterator1.next();
- Object next2 = iterator2.next();
- if ((next1 == null && next2 != null)
- || (next1 != null && !next1.equals(next2))) {
- return false;
- }
- }
- return !iterator1.hasNext() && !iterator2.hasNext();
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append("[");
- Iterator<Map.Entry<K, V>> iterator = iterator();
- while (iterator.hasNext()) {
- builder.append(iterator.next().toString());
- if (iterator.hasNext()) {
- builder.append(", ");
- }
- }
- builder.append("]");
- return builder.toString();
- }
-
- private abstract static class ListIterator<K, V> implements Iterator<Map.Entry<K, V>>,
- SupportRemove<K, V> {
- Entry<K, V> mExpectedEnd;
- Entry<K, V> mNext;
-
- ListIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
- this.mExpectedEnd = expectedEnd;
- this.mNext = start;
- }
-
- @Override
- public boolean hasNext() {
- return mNext != null;
- }
-
- @Override
- public void supportRemove(@NonNull Entry<K, V> entry) {
- if (mExpectedEnd == entry && entry == mNext) {
- mNext = null;
- mExpectedEnd = null;
- }
-
- if (mExpectedEnd == entry) {
- mExpectedEnd = backward(mExpectedEnd);
- }
-
- if (mNext == entry) {
- mNext = nextNode();
- }
- }
-
- private Entry<K, V> nextNode() {
- if (mNext == mExpectedEnd || mExpectedEnd == null) {
- return null;
- }
- return forward(mNext);
- }
-
- @Override
- public Map.Entry<K, V> next() {
- Map.Entry<K, V> result = mNext;
- mNext = nextNode();
- return result;
- }
-
- abstract Entry<K, V> forward(Entry<K, V> entry);
-
- abstract Entry<K, V> backward(Entry<K, V> entry);
- }
-
- static class AscendingIterator<K, V> extends ListIterator<K, V> {
- AscendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
- super(start, expectedEnd);
- }
-
- @Override
- Entry<K, V> forward(Entry<K, V> entry) {
- return entry.mNext;
- }
-
- @Override
- Entry<K, V> backward(Entry<K, V> entry) {
- return entry.mPrevious;
- }
- }
-
- private static class DescendingIterator<K, V> extends ListIterator<K, V> {
-
- DescendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
- super(start, expectedEnd);
- }
-
- @Override
- Entry<K, V> forward(Entry<K, V> entry) {
- return entry.mPrevious;
- }
-
- @Override
- Entry<K, V> backward(Entry<K, V> entry) {
- return entry.mNext;
- }
- }
-
- private class IteratorWithAdditions implements Iterator<Map.Entry<K, V>>, SupportRemove<K, V> {
- private Entry<K, V> mCurrent;
- private boolean mBeforeStart = true;
-
- @Override
- public void supportRemove(@NonNull Entry<K, V> entry) {
- if (entry == mCurrent) {
- mCurrent = mCurrent.mPrevious;
- mBeforeStart = mCurrent == null;
- }
- }
-
- @Override
- public boolean hasNext() {
- if (mBeforeStart) {
- return mStart != null;
- }
- return mCurrent != null && mCurrent.mNext != null;
- }
-
- @Override
- public Map.Entry<K, V> next() {
- if (mBeforeStart) {
- mBeforeStart = false;
- mCurrent = mStart;
- } else {
- mCurrent = mCurrent != null ? mCurrent.mNext : null;
- }
- return mCurrent;
- }
- }
-
- interface SupportRemove<K, V> {
- void supportRemove(@NonNull Entry<K, V> entry);
- }
-
- static class Entry<K, V> implements Map.Entry<K, V> {
- @NonNull
- final K mKey;
- @NonNull
- final V mValue;
- Entry<K, V> mNext;
- Entry<K, V> mPrevious;
-
- Entry(@NonNull K key, @NonNull V value) {
- mKey = key;
- this.mValue = value;
- }
-
- @NonNull
- @Override
- public K getKey() {
- return mKey;
- }
-
- @NonNull
- @Override
- public V getValue() {
- return mValue;
- }
-
- @Override
- public V setValue(V value) {
- throw new UnsupportedOperationException("An entry modification is not supported");
- }
-
- @Override
- public String toString() {
- return mKey + "=" + mValue;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof Entry)) {
- return false;
- }
- Entry entry = (Entry) obj;
- return mKey.equals(entry.mKey) && mValue.equals(entry.mValue);
- }
- }
-}
diff --git a/app-toolkit/common/src/main/java/android/arch/core/util/Function.java b/app-toolkit/common/src/main/java/android/arch/core/util/Function.java
deleted file mode 100644
index 25e7a6b..0000000
--- a/app-toolkit/common/src/main/java/android/arch/core/util/Function.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.core.util;
-
-/**
- * Represents a function.
- *
- * @param <I> the type of the input to the function
- * @param <O> the type of the output of the function
- */
-public interface Function<I, O> {
- /**
- * Applies this function to the given input.
- *
- * @param input the input
- * @return the function result.
- */
- O apply(I input);
-}
diff --git a/app-toolkit/common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java b/app-toolkit/common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java
new file mode 100644
index 0000000..c3a9086
--- /dev/null
+++ b/app-toolkit/common/src/main/java/androidx/arch/core/internal/FastSafeIterableMap.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018 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 androidx.arch.core.internal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Poor's man LinkedHashMap, which supports modifications during iterations.
+ * Takes more memory that {@link SafeIterableMap}
+ * It is NOT thread safe.
+ *
+ * @param <K> Key type
+ * @param <V> Value type
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class FastSafeIterableMap<K, V> extends SafeIterableMap<K, V> {
+
+ private HashMap<K, Entry<K, V>> mHashMap = new HashMap<>();
+
+ @Override
+ protected Entry<K, V> get(K k) {
+ return mHashMap.get(k);
+ }
+
+ @Override
+ public V putIfAbsent(@NonNull K key, @NonNull V v) {
+ Entry<K, V> current = get(key);
+ if (current != null) {
+ return current.mValue;
+ }
+ mHashMap.put(key, put(key, v));
+ return null;
+ }
+
+ @Override
+ public V remove(@NonNull K key) {
+ V removed = super.remove(key);
+ mHashMap.remove(key);
+ return removed;
+ }
+
+ /**
+ * Returns {@code true} if this map contains a mapping for the specified
+ * key.
+ */
+ public boolean contains(K key) {
+ return mHashMap.containsKey(key);
+ }
+
+ /**
+ * Return an entry added to prior to an entry associated with the given key.
+ *
+ * @param k the key
+ */
+ public Map.Entry<K, V> ceil(K k) {
+ if (contains(k)) {
+ return mHashMap.get(k).mPrevious;
+ }
+ return null;
+ }
+}
diff --git a/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java b/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
new file mode 100644
index 0000000..ef09098
--- /dev/null
+++ b/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2018 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 androidx.arch.core.internal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * LinkedList, which pretends to be a map and supports modifications during iterations.
+ * It is NOT thread safe.
+ *
+ * @param <K> Key type
+ * @param <V> Value type
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SafeIterableMap<K, V> implements Iterable<Map.Entry<K, V>> {
+
+ private Entry<K, V> mStart;
+ private Entry<K, V> mEnd;
+ // using WeakHashMap over List<WeakReference>, so we don't have to manually remove
+ // WeakReferences that have null in them.
+ private WeakHashMap<SupportRemove<K, V>, Boolean> mIterators = new WeakHashMap<>();
+ private int mSize = 0;
+
+ protected Entry<K, V> get(K k) {
+ Entry<K, V> currentNode = mStart;
+ while (currentNode != null) {
+ if (currentNode.mKey.equals(k)) {
+ break;
+ }
+ currentNode = currentNode.mNext;
+ }
+ return currentNode;
+ }
+
+ /**
+ * If the specified key is not already associated
+ * with a value, associates it with the given value.
+ *
+ * @param key key with which the specified value is to be associated
+ * @param v value to be associated with the specified key
+ * @return the previous value associated with the specified key,
+ * or {@code null} if there was no mapping for the key
+ */
+ public V putIfAbsent(@NonNull K key, @NonNull V v) {
+ Entry<K, V> entry = get(key);
+ if (entry != null) {
+ return entry.mValue;
+ }
+ put(key, v);
+ return null;
+ }
+
+ protected Entry<K, V> put(@NonNull K key, @NonNull V v) {
+ Entry<K, V> newEntry = new Entry<>(key, v);
+ mSize++;
+ if (mEnd == null) {
+ mStart = newEntry;
+ mEnd = mStart;
+ return newEntry;
+ }
+
+ mEnd.mNext = newEntry;
+ newEntry.mPrevious = mEnd;
+ mEnd = newEntry;
+ return newEntry;
+
+ }
+
+ /**
+ * Removes the mapping for a key from this map if it is present.
+ *
+ * @param key key whose mapping is to be removed from the map
+ * @return the previous value associated with the specified key,
+ * or {@code null} if there was no mapping for the key
+ */
+ public V remove(@NonNull K key) {
+ Entry<K, V> toRemove = get(key);
+ if (toRemove == null) {
+ return null;
+ }
+ mSize--;
+ if (!mIterators.isEmpty()) {
+ for (SupportRemove<K, V> iter : mIterators.keySet()) {
+ iter.supportRemove(toRemove);
+ }
+ }
+
+ if (toRemove.mPrevious != null) {
+ toRemove.mPrevious.mNext = toRemove.mNext;
+ } else {
+ mStart = toRemove.mNext;
+ }
+
+ if (toRemove.mNext != null) {
+ toRemove.mNext.mPrevious = toRemove.mPrevious;
+ } else {
+ mEnd = toRemove.mPrevious;
+ }
+
+ toRemove.mNext = null;
+ toRemove.mPrevious = null;
+ return toRemove.mValue;
+ }
+
+ /**
+ * @return the number of elements in this map
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * @return an ascending iterator, which doesn't include new elements added during an
+ * iteration.
+ */
+ @NonNull
+ @Override
+ public Iterator<Map.Entry<K, V>> iterator() {
+ ListIterator<K, V> iterator = new AscendingIterator<>(mStart, mEnd);
+ mIterators.put(iterator, false);
+ return iterator;
+ }
+
+ /**
+ * @return an descending iterator, which doesn't include new elements added during an
+ * iteration.
+ */
+ public Iterator<Map.Entry<K, V>> descendingIterator() {
+ DescendingIterator<K, V> iterator = new DescendingIterator<>(mEnd, mStart);
+ mIterators.put(iterator, false);
+ return iterator;
+ }
+
+ /**
+ * return an iterator with additions.
+ */
+ public IteratorWithAdditions iteratorWithAdditions() {
+ @SuppressWarnings("unchecked")
+ IteratorWithAdditions iterator = new IteratorWithAdditions();
+ mIterators.put(iterator, false);
+ return iterator;
+ }
+
+ /**
+ * @return eldest added entry or null
+ */
+ public Map.Entry<K, V> eldest() {
+ return mStart;
+ }
+
+ /**
+ * @return newest added entry or null
+ */
+ public Map.Entry<K, V> newest() {
+ return mEnd;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof SafeIterableMap)) {
+ return false;
+ }
+ SafeIterableMap map = (SafeIterableMap) obj;
+ if (this.size() != map.size()) {
+ return false;
+ }
+ Iterator<Map.Entry<K, V>> iterator1 = iterator();
+ Iterator iterator2 = map.iterator();
+ while (iterator1.hasNext() && iterator2.hasNext()) {
+ Map.Entry<K, V> next1 = iterator1.next();
+ Object next2 = iterator2.next();
+ if ((next1 == null && next2 != null)
+ || (next1 != null && !next1.equals(next2))) {
+ return false;
+ }
+ }
+ return !iterator1.hasNext() && !iterator2.hasNext();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ Iterator<Map.Entry<K, V>> iterator = iterator();
+ while (iterator.hasNext()) {
+ builder.append(iterator.next().toString());
+ if (iterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ private abstract static class ListIterator<K, V> implements Iterator<Map.Entry<K, V>>,
+ SupportRemove<K, V> {
+ Entry<K, V> mExpectedEnd;
+ Entry<K, V> mNext;
+
+ ListIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
+ this.mExpectedEnd = expectedEnd;
+ this.mNext = start;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mNext != null;
+ }
+
+ @Override
+ public void supportRemove(@NonNull Entry<K, V> entry) {
+ if (mExpectedEnd == entry && entry == mNext) {
+ mNext = null;
+ mExpectedEnd = null;
+ }
+
+ if (mExpectedEnd == entry) {
+ mExpectedEnd = backward(mExpectedEnd);
+ }
+
+ if (mNext == entry) {
+ mNext = nextNode();
+ }
+ }
+
+ private Entry<K, V> nextNode() {
+ if (mNext == mExpectedEnd || mExpectedEnd == null) {
+ return null;
+ }
+ return forward(mNext);
+ }
+
+ @Override
+ public Map.Entry<K, V> next() {
+ Map.Entry<K, V> result = mNext;
+ mNext = nextNode();
+ return result;
+ }
+
+ abstract Entry<K, V> forward(Entry<K, V> entry);
+
+ abstract Entry<K, V> backward(Entry<K, V> entry);
+ }
+
+ static class AscendingIterator<K, V> extends ListIterator<K, V> {
+ AscendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
+ super(start, expectedEnd);
+ }
+
+ @Override
+ Entry<K, V> forward(Entry<K, V> entry) {
+ return entry.mNext;
+ }
+
+ @Override
+ Entry<K, V> backward(Entry<K, V> entry) {
+ return entry.mPrevious;
+ }
+ }
+
+ private static class DescendingIterator<K, V> extends ListIterator<K, V> {
+
+ DescendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
+ super(start, expectedEnd);
+ }
+
+ @Override
+ Entry<K, V> forward(Entry<K, V> entry) {
+ return entry.mPrevious;
+ }
+
+ @Override
+ Entry<K, V> backward(Entry<K, V> entry) {
+ return entry.mNext;
+ }
+ }
+
+ private class IteratorWithAdditions implements Iterator<Map.Entry<K, V>>, SupportRemove<K, V> {
+ private Entry<K, V> mCurrent;
+ private boolean mBeforeStart = true;
+
+ @Override
+ public void supportRemove(@NonNull Entry<K, V> entry) {
+ if (entry == mCurrent) {
+ mCurrent = mCurrent.mPrevious;
+ mBeforeStart = mCurrent == null;
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (mBeforeStart) {
+ return mStart != null;
+ }
+ return mCurrent != null && mCurrent.mNext != null;
+ }
+
+ @Override
+ public Map.Entry<K, V> next() {
+ if (mBeforeStart) {
+ mBeforeStart = false;
+ mCurrent = mStart;
+ } else {
+ mCurrent = mCurrent != null ? mCurrent.mNext : null;
+ }
+ return mCurrent;
+ }
+ }
+
+ interface SupportRemove<K, V> {
+ void supportRemove(@NonNull Entry<K, V> entry);
+ }
+
+ static class Entry<K, V> implements Map.Entry<K, V> {
+ @NonNull
+ final K mKey;
+ @NonNull
+ final V mValue;
+ Entry<K, V> mNext;
+ Entry<K, V> mPrevious;
+
+ Entry(@NonNull K key, @NonNull V value) {
+ mKey = key;
+ this.mValue = value;
+ }
+
+ @NonNull
+ @Override
+ public K getKey() {
+ return mKey;
+ }
+
+ @NonNull
+ @Override
+ public V getValue() {
+ return mValue;
+ }
+
+ @Override
+ public V setValue(V value) {
+ throw new UnsupportedOperationException("An entry modification is not supported");
+ }
+
+ @Override
+ public String toString() {
+ return mKey + "=" + mValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof Entry)) {
+ return false;
+ }
+ Entry entry = (Entry) obj;
+ return mKey.equals(entry.mKey) && mValue.equals(entry.mValue);
+ }
+ }
+}
diff --git a/app-toolkit/common/src/main/java/androidx/arch/core/util/Function.java b/app-toolkit/common/src/main/java/androidx/arch/core/util/Function.java
new file mode 100644
index 0000000..72bde2f
--- /dev/null
+++ b/app-toolkit/common/src/main/java/androidx/arch/core/util/Function.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 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 androidx.arch.core.util;
+
+/**
+ * Represents a function.
+ *
+ * @param <I> the type of the input to the function
+ * @param <O> the type of the output of the function
+ */
+public interface Function<I, O> {
+ /**
+ * Applies this function to the given input.
+ *
+ * @param input the input
+ * @return the function result.
+ */
+ O apply(I input);
+}
diff --git a/app-toolkit/common/src/test/java/android/arch/core/internal/FastSafeIterableMapTest.java b/app-toolkit/common/src/test/java/android/arch/core/internal/FastSafeIterableMapTest.java
deleted file mode 100644
index 41b1497..0000000
--- a/app-toolkit/common/src/test/java/android/arch/core/internal/FastSafeIterableMapTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.core.internal;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.junit.Assert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class FastSafeIterableMapTest {
- @Test
- public void testCeil() {
- FastSafeIterableMap<Integer, Boolean> map = new FastSafeIterableMap<>();
- assertThat(map.ceil(1), nullValue());
- map.putIfAbsent(1, false);
- assertThat(map.ceil(1), nullValue());
- map.putIfAbsent(2, false);
- assertThat(map.ceil(2).getKey(), is(1));
- map.remove(1);
- assertThat(map.ceil(2), nullValue());
- }
-
- @Test
- public void testPut() {
- FastSafeIterableMap<Integer, Integer> map = new FastSafeIterableMap<>();
- map.putIfAbsent(10, 20);
- map.putIfAbsent(20, 40);
- map.putIfAbsent(30, 60);
- assertThat(map.putIfAbsent(5, 10), is((Integer) null));
- assertThat(map.putIfAbsent(10, 30), is(20));
- }
-
- @Test
- public void testContains() {
- FastSafeIterableMap<Integer, Integer> map = new FastSafeIterableMap<>();
- map.putIfAbsent(10, 20);
- map.putIfAbsent(20, 40);
- map.putIfAbsent(30, 60);
- assertThat(map.contains(10), is(true));
- assertThat(map.contains(11), is(false));
- assertThat(new FastSafeIterableMap<Integer, Integer>().contains(0), is(false));
- }
-
-
- @Test
- public void testRemove() {
- FastSafeIterableMap<Integer, Integer> map = new FastSafeIterableMap<>();
- map.putIfAbsent(10, 20);
- map.putIfAbsent(20, 40);
- assertThat(map.contains(10), is(true));
- assertThat(map.contains(20), is(true));
- assertThat(map.remove(10), is(20));
- assertThat(map.contains(10), is(false));
- assertThat(map.putIfAbsent(10, 30), nullValue());
- assertThat(map.putIfAbsent(10, 40), is(30));
- }
-}
diff --git a/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java b/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java
deleted file mode 100644
index d879543..0000000
--- a/app-toolkit/common/src/test/java/android/arch/core/internal/SafeIterableMapTest.java
+++ /dev/null
@@ -1,505 +0,0 @@
-/*
- * 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.core.internal;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Map.Entry;
-
-@RunWith(JUnit4.class)
-public class SafeIterableMapTest {
-
- @Test
- public void testToString() {
- SafeIterableMap<Integer, String> map = from(1, 2, 3, 4).to("a", "b", "c", "d");
- assertThat(map.toString(), is("[1=a, 2=b, 3=c, 4=d]"));
- }
-
- @Test
- public void testEmptyToString() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- assertThat(map.toString(), is("[]"));
- }
-
- @Test
- public void testOneElementToString() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1);
- assertThat(map.toString(), is("[1=true]"));
- }
-
-
- @Test
- public void testEquality1() {
- SafeIterableMap<Integer, Integer> map1 = from(1, 2, 3, 4).to(10, 20, 30, 40);
- SafeIterableMap<Integer, Integer> map2 = from(1, 2, 3, 4).to(10, 20, 30, 40);
- assertThat(map1.equals(map2), is(true));
- }
-
- @Test
- public void testEquality2() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- //noinspection ObjectEqualsNull
- assertThat(map.equals(null), is(false));
- }
-
- @Test
- public void testEquality3() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- //noinspection EqualsBetweenInconvertibleTypes
- assertThat(map.equals(new ArrayList<>()), is(false));
- }
-
- @Test
- public void testEquality4() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- assertThat(map.equals(new SafeIterableMap<Integer, Boolean>()), is(false));
- }
-
- @Test
- public void testEquality5() {
- SafeIterableMap<Integer, Boolean> map1 = mapOf(1, 2, 3, 4);
- SafeIterableMap<Integer, Boolean> map2 = mapOf(1);
- assertThat(map1.equals(map2), is(false));
- }
-
- @Test
- public void testEquality6() {
- SafeIterableMap<Integer, Boolean> map1 = mapOf(1, 2, 3, 4);
- SafeIterableMap<Integer, Boolean> map2 = mapOf(1, 2, 3, 5);
- assertThat(map1.equals(map2), is(false));
- }
-
- @Test
- public void testEquality7() {
- SafeIterableMap<Integer, Integer> map1 = from(1, 2, 3, 4).to(1, 2, 3, 4);
- SafeIterableMap<Integer, Integer> map2 = from(1, 2, 3, 4).to(1, 2, 3, 5);
- assertThat(map1.equals(map2), is(false));
- }
-
-
- @Test
- public void testEquality8() {
- SafeIterableMap<Integer, Boolean> map1 = mapOf();
- SafeIterableMap<Integer, Boolean> map2 = mapOf();
- assertThat(map1.equals(map2), is(true));
- }
-
- @Test
- public void testEqualityRespectsOrder() {
- SafeIterableMap<Integer, Boolean> map1 = mapOf(1, 2, 3, 4);
- SafeIterableMap<Integer, Boolean> map2 = mapOf(1, 3, 2, 4);
- assertThat(map1.equals(map2), is(false));
- }
-
- @Test
- public void testPut() {
- SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
- assertThat(map.putIfAbsent(5, 10), is((Integer) null));
- assertThat(map, is(from(1, 2, 3, 4, 5).to(10, 20, 30, 40, 10)));
- }
-
- @Test
- public void testAddExisted() {
- SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 261, 40);
- assertThat(map.putIfAbsent(3, 239), is(261));
- assertThat(map, is(from(1, 2, 3, 4).to(10, 20, 261, 40)));
- }
-
- @Test
- public void testRemoveLast() {
- SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
- assertThat(map.remove(4), is(40));
- assertThat(map, is(from(1, 2, 3).to(10, 20, 30)));
- }
-
- @Test
- public void testRemoveFirst() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- assertThat(map.remove(1), is(true));
- assertThat(map, is(mapOf(2, 3, 4)));
- }
-
- @Test
- public void testRemoveMiddle() {
- SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
- assertThat(map.remove(2), is(20));
- assertThat(map.remove(3), is(30));
- assertThat(map, is(from(1, 4).to(10, 40)));
- }
-
- @Test
- public void testRemoveNotExisted() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- assertThat(map.remove(5), is((Boolean) null));
- assertThat(map, is(mapOf(1, 2, 3, 4)));
- }
-
- @Test
- public void testRemoveSole() {
- SafeIterableMap<Integer, Integer> map = from(1).to(261);
- assertThat(map.remove(1), is(261));
- assertThat(map, is(new SafeIterableMap<Integer, Integer>()));
- }
-
- @Test
- public void testRemoveDuringIteration1() {
- SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
- int index = 0;
- int[] expected = new int[]{1, 4};
- for (Entry<Integer, Integer> i : map) {
- assertThat(i.getKey(), is(expected[index++]));
- if (index == 1) {
- assertThat(map.remove(2), is(20));
- assertThat(map.remove(3), is(30));
- }
- }
- }
-
- @Test
- public void testRemoveDuringIteration2() {
- SafeIterableMap<Integer, Integer> map = from(1, 2).to(10, 20);
- Iterator<Entry<Integer, Integer>> iter = map.iterator();
- assertThat(map.remove(2), is(20));
- assertThat(map.remove(1), is(10));
- assertThat(iter.hasNext(), is(false));
- }
-
- @Test
- public void testRemoveDuringIteration3() {
- SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
- int index = 0;
- Iterator<Entry<Integer, Integer>> iter = map.iterator();
- assertThat(map.remove(1), is(10));
- assertThat(map.remove(2), is(20));
- int[] expected = new int[]{3, 4};
- while (iter.hasNext()) {
- assertThat(iter.next().getKey(), is(expected[index++]));
- }
- }
-
- @Test
- public void testRemoveDuringIteration4() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2);
- int[] expected = new int[]{1, 2};
- int index = 0;
- for (Entry<Integer, Boolean> entry : map) {
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 1) {
- map.remove(1);
- }
- }
- assertThat(index, is(2));
- }
-
- @Test
- public void testAdditionDuringIteration() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{1, 2, 3, 4};
- int index = 0;
- for (Entry<Integer, Boolean> entry : map) {
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 1) {
- map.putIfAbsent(5, true);
- }
- }
- }
-
- @Test
- public void testReAdditionDuringIteration() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{1, 2, 4};
- int index = 0;
- for (Entry<Integer, Boolean> entry : map) {
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 1) {
- map.remove(3);
- map.putIfAbsent(3, true);
- }
- }
- }
-
- @Test
- public void testSize() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- assertThat(map.size(), is(4));
- map.putIfAbsent(5, true);
- map.putIfAbsent(6, true);
- assertThat(map.size(), is(6));
- map.remove(5);
- map.remove(5);
- assertThat(map.size(), is(5));
- map.remove(1);
- map.remove(2);
- map.remove(4);
- map.remove(3);
- map.remove(6);
- assertThat(map.size(), is(0));
- map.putIfAbsent(4, true);
- assertThat(map.size(), is(1));
- assertThat(mapOf().size(), is(0));
- }
-
- @Test
- public void testIteratorWithAdditions1() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{1, 2, 3, 5};
- int index = 0;
- Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
- while (iterator.hasNext()) {
- Entry<Integer, Boolean> entry = iterator.next();
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 3) {
- map.remove(4);
- map.putIfAbsent(5, true);
- }
- }
- }
-
- @Test
- public void testIteratorWithAdditions2() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1);
- int[] expected = new int[]{1, 2, 3};
- int index = 0;
- Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
- while (iterator.hasNext()) {
- Entry<Integer, Boolean> entry = iterator.next();
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 1) {
- map.putIfAbsent(2, true);
- map.putIfAbsent(3, true);
- }
- }
- assertThat(index, is(3));
- }
-
-
- @Test
- public void testIteratorWithAdditions3() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3);
- int[] expected = new int[]{1};
- int index = 0;
- Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
- while (iterator.hasNext()) {
- Entry<Integer, Boolean> entry = iterator.next();
- assertThat(entry.getKey(), is(expected[index++]));
- map.remove(2);
- map.remove(3);
- }
- assertThat(index, is(1));
- }
-
- @Test
- public void testIteratorWithAdditions4() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- int[] expected = new int[]{1, 2, 3};
- int index = 0;
- Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
- map.putIfAbsent(1, true);
- while (iterator.hasNext()) {
- Entry<Integer, Boolean> entry = iterator.next();
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 1) {
- map.putIfAbsent(2, false);
- }
- if (index == 2) {
- map.putIfAbsent(3, false);
- }
- }
- assertThat(index, is(3));
- }
-
- @Test
- public void testIteratorWithAddition5() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2);
- int[] expected = new int[]{1, 2};
- int index = 0;
- Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
- while (iterator.hasNext()) {
- Entry<Integer, Boolean> entry = iterator.next();
- assertThat(entry.getKey(), is(expected[index++]));
- if (index == 1) {
- map.remove(1);
- }
- }
- assertThat(index, is(2));
- }
-
- @Test
- public void testDescendingIteration() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{4, 3, 2, 1};
- int index = 0;
- for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
- assertThat(iter.next().getKey(), is(expected[index++]));
- }
- assertThat(index, is(4));
- }
-
- @Test
- public void testDescendingIterationRemove1() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{4, 3, 2};
- int index = 0;
- for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
- if (index == 1) {
- map.remove(1);
- }
- assertThat(iter.next().getKey(), is(expected[index++]));
- }
- assertThat(index, is(3));
- assertThat(map.size(), is(3));
- }
-
- @Test
- public void testDescendingIterationRemove2() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{3, 2, 1};
- int index = 0;
- for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
- if (index == 0) {
- map.remove(4);
- }
- assertThat(iter.next().getKey(), is(expected[index++]));
- }
- assertThat(index, is(3));
- assertThat(map.size(), is(3));
- }
-
- @Test
- public void testDescendingIterationRemove3() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{4, 1};
- int index = 0;
- for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
- if (index == 1) {
- map.remove(3);
- map.remove(2);
- }
- assertThat(iter.next().getKey(), is(expected[index++]));
- }
- assertThat(index, is(2));
- assertThat(map.size(), is(2));
- }
-
- @Test
- public void testDescendingIterationAddition() {
- SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
- int[] expected = new int[]{4, 3, 2, 1};
- int index = 0;
- for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
- if (index == 0) {
- map.putIfAbsent(5, false);
- }
- assertThat(iter.next().getKey(), is(expected[index++]));
- }
- assertThat(index, is(4));
- assertThat(map.size(), is(5));
- }
-
- @Test
- public void testDescendingIteratorEmpty() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- Iterator<Entry<Integer, Boolean>> iterator = map.descendingIterator();
- assertThat(iterator.hasNext(), is(false));
- }
-
- @Test
- public void testIteratorEmpty() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- Iterator<Entry<Integer, Boolean>> iterator = map.iterator();
- assertThat(iterator.hasNext(), is(false));
- }
-
- @Test
- public void testIteratorWithAdditionEmpty() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
- assertThat(iterator.hasNext(), is(false));
- }
-
- @Test
- public void testEldest() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- assertThat(map.eldest(), nullValue());
- map.putIfAbsent(1, false);
- assertThat(map.eldest().getKey(), is(1));
- map.putIfAbsent(2, false);
- assertThat(map.eldest().getKey(), is(1));
- map.remove(1);
- assertThat(map.eldest().getKey(), is(2));
- map.remove(2);
- assertThat(map.eldest(), nullValue());
- }
-
- @Test
- public void testNewest() {
- SafeIterableMap<Integer, Boolean> map = mapOf();
- assertThat(map.newest(), nullValue());
- map.putIfAbsent(1, false);
- assertThat(map.newest().getKey(), is(1));
- map.putIfAbsent(2, false);
- assertThat(map.newest().getKey(), is(2));
- map.remove(2);
- assertThat(map.eldest().getKey(), is(1));
- map.remove(1);
- assertThat(map.newest(), nullValue());
- }
-
-
- // for most operations we don't care about values, so we create map from key to true
- @SafeVarargs
- private static <K> SafeIterableMap<K, Boolean> mapOf(K... keys) {
- SafeIterableMap<K, Boolean> map = new SafeIterableMap<>();
- for (K key : keys) {
- map.putIfAbsent(key, true);
- }
- return map;
- }
-
- @SafeVarargs
- private static <K> MapBuilder<K> from(K... keys) {
- return new MapBuilder<>(keys);
- }
-
- private static class MapBuilder<K> {
- final K[] mKeys;
-
- MapBuilder(K[] keys) {
- this.mKeys = keys;
- }
-
- @SafeVarargs
- public final <V> SafeIterableMap<K, V> to(V... values) {
- assertThat("Failed to build Map", mKeys.length, is(values.length));
- SafeIterableMap<K, V> map = new SafeIterableMap<>();
- for (int i = 0; i < mKeys.length; i++) {
- map.putIfAbsent(mKeys[i], values[i]);
- }
- return map;
- }
- }
-}
-
-
diff --git a/app-toolkit/common/src/test/java/androidx/collection/FastSafeIterableMapTest.java b/app-toolkit/common/src/test/java/androidx/collection/FastSafeIterableMapTest.java
new file mode 100644
index 0000000..9b392e2
--- /dev/null
+++ b/app-toolkit/common/src/test/java/androidx/collection/FastSafeIterableMapTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018 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 androidx.collection;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+import androidx.arch.core.internal.FastSafeIterableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FastSafeIterableMapTest {
+ @Test
+ public void testCeil() {
+ FastSafeIterableMap<Integer, Boolean> map = new FastSafeIterableMap<>();
+ assertThat(map.ceil(1), nullValue());
+ map.putIfAbsent(1, false);
+ assertThat(map.ceil(1), nullValue());
+ map.putIfAbsent(2, false);
+ assertThat(map.ceil(2).getKey(), is(1));
+ map.remove(1);
+ assertThat(map.ceil(2), nullValue());
+ }
+
+ @Test
+ public void testPut() {
+ FastSafeIterableMap<Integer, Integer> map = new FastSafeIterableMap<>();
+ map.putIfAbsent(10, 20);
+ map.putIfAbsent(20, 40);
+ map.putIfAbsent(30, 60);
+ assertThat(map.putIfAbsent(5, 10), is((Integer) null));
+ assertThat(map.putIfAbsent(10, 30), is(20));
+ }
+
+ @Test
+ public void testContains() {
+ FastSafeIterableMap<Integer, Integer> map = new FastSafeIterableMap<>();
+ map.putIfAbsent(10, 20);
+ map.putIfAbsent(20, 40);
+ map.putIfAbsent(30, 60);
+ assertThat(map.contains(10), is(true));
+ assertThat(map.contains(11), is(false));
+ assertThat(new FastSafeIterableMap<Integer, Integer>().contains(0), is(false));
+ }
+
+
+ @Test
+ public void testRemove() {
+ FastSafeIterableMap<Integer, Integer> map = new FastSafeIterableMap<>();
+ map.putIfAbsent(10, 20);
+ map.putIfAbsent(20, 40);
+ assertThat(map.contains(10), is(true));
+ assertThat(map.contains(20), is(true));
+ assertThat(map.remove(10), is(20));
+ assertThat(map.contains(10), is(false));
+ assertThat(map.putIfAbsent(10, 30), nullValue());
+ assertThat(map.putIfAbsent(10, 40), is(30));
+ }
+}
diff --git a/app-toolkit/common/src/test/java/androidx/collection/SafeIterableMapTest.java b/app-toolkit/common/src/test/java/androidx/collection/SafeIterableMapTest.java
new file mode 100644
index 0000000..f425f6b
--- /dev/null
+++ b/app-toolkit/common/src/test/java/androidx/collection/SafeIterableMapTest.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright 2018 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 androidx.collection;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import androidx.arch.core.internal.SafeIterableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+@RunWith(JUnit4.class)
+public class SafeIterableMapTest {
+
+ @Test
+ public void testToString() {
+ SafeIterableMap<Integer, String> map = from(1, 2, 3, 4).to("a", "b", "c", "d");
+ assertThat(map.toString(), is("[1=a, 2=b, 3=c, 4=d]"));
+ }
+
+ @Test
+ public void testEmptyToString() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ assertThat(map.toString(), is("[]"));
+ }
+
+ @Test
+ public void testOneElementToString() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1);
+ assertThat(map.toString(), is("[1=true]"));
+ }
+
+
+ @Test
+ public void testEquality1() {
+ SafeIterableMap<Integer, Integer> map1 = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ SafeIterableMap<Integer, Integer> map2 = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ assertThat(map1.equals(map2), is(true));
+ }
+
+ @Test
+ public void testEquality2() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ //noinspection ObjectEqualsNull
+ assertThat(map.equals(null), is(false));
+ }
+
+ @Test
+ public void testEquality3() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertThat(map.equals(new ArrayList<>()), is(false));
+ }
+
+ @Test
+ public void testEquality4() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ assertThat(map.equals(new SafeIterableMap<Integer, Boolean>()), is(false));
+ }
+
+ @Test
+ public void testEquality5() {
+ SafeIterableMap<Integer, Boolean> map1 = mapOf(1, 2, 3, 4);
+ SafeIterableMap<Integer, Boolean> map2 = mapOf(1);
+ assertThat(map1.equals(map2), is(false));
+ }
+
+ @Test
+ public void testEquality6() {
+ SafeIterableMap<Integer, Boolean> map1 = mapOf(1, 2, 3, 4);
+ SafeIterableMap<Integer, Boolean> map2 = mapOf(1, 2, 3, 5);
+ assertThat(map1.equals(map2), is(false));
+ }
+
+ @Test
+ public void testEquality7() {
+ SafeIterableMap<Integer, Integer> map1 = from(1, 2, 3, 4).to(1, 2, 3, 4);
+ SafeIterableMap<Integer, Integer> map2 = from(1, 2, 3, 4).to(1, 2, 3, 5);
+ assertThat(map1.equals(map2), is(false));
+ }
+
+
+ @Test
+ public void testEquality8() {
+ SafeIterableMap<Integer, Boolean> map1 = mapOf();
+ SafeIterableMap<Integer, Boolean> map2 = mapOf();
+ assertThat(map1.equals(map2), is(true));
+ }
+
+ @Test
+ public void testEqualityRespectsOrder() {
+ SafeIterableMap<Integer, Boolean> map1 = mapOf(1, 2, 3, 4);
+ SafeIterableMap<Integer, Boolean> map2 = mapOf(1, 3, 2, 4);
+ assertThat(map1.equals(map2), is(false));
+ }
+
+ @Test
+ public void testPut() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ assertThat(map.putIfAbsent(5, 10), is((Integer) null));
+ assertThat(map, is(from(1, 2, 3, 4, 5).to(10, 20, 30, 40, 10)));
+ }
+
+ @Test
+ public void testAddExisted() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 261, 40);
+ assertThat(map.putIfAbsent(3, 239), is(261));
+ assertThat(map, is(from(1, 2, 3, 4).to(10, 20, 261, 40)));
+ }
+
+ @Test
+ public void testRemoveLast() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ assertThat(map.remove(4), is(40));
+ assertThat(map, is(from(1, 2, 3).to(10, 20, 30)));
+ }
+
+ @Test
+ public void testRemoveFirst() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ assertThat(map.remove(1), is(true));
+ assertThat(map, is(mapOf(2, 3, 4)));
+ }
+
+ @Test
+ public void testRemoveMiddle() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ assertThat(map.remove(2), is(20));
+ assertThat(map.remove(3), is(30));
+ assertThat(map, is(from(1, 4).to(10, 40)));
+ }
+
+ @Test
+ public void testRemoveNotExisted() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ assertThat(map.remove(5), is((Boolean) null));
+ assertThat(map, is(mapOf(1, 2, 3, 4)));
+ }
+
+ @Test
+ public void testRemoveSole() {
+ SafeIterableMap<Integer, Integer> map = from(1).to(261);
+ assertThat(map.remove(1), is(261));
+ assertThat(map, is(new SafeIterableMap<Integer, Integer>()));
+ }
+
+ @Test
+ public void testRemoveDuringIteration1() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ int index = 0;
+ int[] expected = new int[]{1, 4};
+ for (Entry<Integer, Integer> i : map) {
+ assertThat(i.getKey(), is(expected[index++]));
+ if (index == 1) {
+ assertThat(map.remove(2), is(20));
+ assertThat(map.remove(3), is(30));
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveDuringIteration2() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2).to(10, 20);
+ Iterator<Entry<Integer, Integer>> iter = map.iterator();
+ assertThat(map.remove(2), is(20));
+ assertThat(map.remove(1), is(10));
+ assertThat(iter.hasNext(), is(false));
+ }
+
+ @Test
+ public void testRemoveDuringIteration3() {
+ SafeIterableMap<Integer, Integer> map = from(1, 2, 3, 4).to(10, 20, 30, 40);
+ int index = 0;
+ Iterator<Entry<Integer, Integer>> iter = map.iterator();
+ assertThat(map.remove(1), is(10));
+ assertThat(map.remove(2), is(20));
+ int[] expected = new int[]{3, 4};
+ while (iter.hasNext()) {
+ assertThat(iter.next().getKey(), is(expected[index++]));
+ }
+ }
+
+ @Test
+ public void testRemoveDuringIteration4() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2);
+ int[] expected = new int[]{1, 2};
+ int index = 0;
+ for (Entry<Integer, Boolean> entry : map) {
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.remove(1);
+ }
+ }
+ assertThat(index, is(2));
+ }
+
+ @Test
+ public void testAdditionDuringIteration() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{1, 2, 3, 4};
+ int index = 0;
+ for (Entry<Integer, Boolean> entry : map) {
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.putIfAbsent(5, true);
+ }
+ }
+ }
+
+ @Test
+ public void testReAdditionDuringIteration() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{1, 2, 4};
+ int index = 0;
+ for (Entry<Integer, Boolean> entry : map) {
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.remove(3);
+ map.putIfAbsent(3, true);
+ }
+ }
+ }
+
+ @Test
+ public void testSize() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ assertThat(map.size(), is(4));
+ map.putIfAbsent(5, true);
+ map.putIfAbsent(6, true);
+ assertThat(map.size(), is(6));
+ map.remove(5);
+ map.remove(5);
+ assertThat(map.size(), is(5));
+ map.remove(1);
+ map.remove(2);
+ map.remove(4);
+ map.remove(3);
+ map.remove(6);
+ assertThat(map.size(), is(0));
+ map.putIfAbsent(4, true);
+ assertThat(map.size(), is(1));
+ assertThat(mapOf().size(), is(0));
+ }
+
+ @Test
+ public void testIteratorWithAdditions1() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{1, 2, 3, 5};
+ int index = 0;
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ while (iterator.hasNext()) {
+ Entry<Integer, Boolean> entry = iterator.next();
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 3) {
+ map.remove(4);
+ map.putIfAbsent(5, true);
+ }
+ }
+ }
+
+ @Test
+ public void testIteratorWithAdditions2() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1);
+ int[] expected = new int[]{1, 2, 3};
+ int index = 0;
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ while (iterator.hasNext()) {
+ Entry<Integer, Boolean> entry = iterator.next();
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.putIfAbsent(2, true);
+ map.putIfAbsent(3, true);
+ }
+ }
+ assertThat(index, is(3));
+ }
+
+
+ @Test
+ public void testIteratorWithAdditions3() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3);
+ int[] expected = new int[]{1};
+ int index = 0;
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ while (iterator.hasNext()) {
+ Entry<Integer, Boolean> entry = iterator.next();
+ assertThat(entry.getKey(), is(expected[index++]));
+ map.remove(2);
+ map.remove(3);
+ }
+ assertThat(index, is(1));
+ }
+
+ @Test
+ public void testIteratorWithAdditions4() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ int[] expected = new int[]{1, 2, 3};
+ int index = 0;
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ map.putIfAbsent(1, true);
+ while (iterator.hasNext()) {
+ Entry<Integer, Boolean> entry = iterator.next();
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.putIfAbsent(2, false);
+ }
+ if (index == 2) {
+ map.putIfAbsent(3, false);
+ }
+ }
+ assertThat(index, is(3));
+ }
+
+ @Test
+ public void testIteratorWithAddition5() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2);
+ int[] expected = new int[]{1, 2};
+ int index = 0;
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ while (iterator.hasNext()) {
+ Entry<Integer, Boolean> entry = iterator.next();
+ assertThat(entry.getKey(), is(expected[index++]));
+ if (index == 1) {
+ map.remove(1);
+ }
+ }
+ assertThat(index, is(2));
+ }
+
+ @Test
+ public void testDescendingIteration() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{4, 3, 2, 1};
+ int index = 0;
+ for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
+ assertThat(iter.next().getKey(), is(expected[index++]));
+ }
+ assertThat(index, is(4));
+ }
+
+ @Test
+ public void testDescendingIterationRemove1() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{4, 3, 2};
+ int index = 0;
+ for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
+ if (index == 1) {
+ map.remove(1);
+ }
+ assertThat(iter.next().getKey(), is(expected[index++]));
+ }
+ assertThat(index, is(3));
+ assertThat(map.size(), is(3));
+ }
+
+ @Test
+ public void testDescendingIterationRemove2() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{3, 2, 1};
+ int index = 0;
+ for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
+ if (index == 0) {
+ map.remove(4);
+ }
+ assertThat(iter.next().getKey(), is(expected[index++]));
+ }
+ assertThat(index, is(3));
+ assertThat(map.size(), is(3));
+ }
+
+ @Test
+ public void testDescendingIterationRemove3() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{4, 1};
+ int index = 0;
+ for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
+ if (index == 1) {
+ map.remove(3);
+ map.remove(2);
+ }
+ assertThat(iter.next().getKey(), is(expected[index++]));
+ }
+ assertThat(index, is(2));
+ assertThat(map.size(), is(2));
+ }
+
+ @Test
+ public void testDescendingIterationAddition() {
+ SafeIterableMap<Integer, Boolean> map = mapOf(1, 2, 3, 4);
+ int[] expected = new int[]{4, 3, 2, 1};
+ int index = 0;
+ for (Iterator<Entry<Integer, Boolean>> iter = map.descendingIterator(); iter.hasNext(); ) {
+ if (index == 0) {
+ map.putIfAbsent(5, false);
+ }
+ assertThat(iter.next().getKey(), is(expected[index++]));
+ }
+ assertThat(index, is(4));
+ assertThat(map.size(), is(5));
+ }
+
+ @Test
+ public void testDescendingIteratorEmpty() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ Iterator<Entry<Integer, Boolean>> iterator = map.descendingIterator();
+ assertThat(iterator.hasNext(), is(false));
+ }
+
+ @Test
+ public void testIteratorEmpty() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ Iterator<Entry<Integer, Boolean>> iterator = map.iterator();
+ assertThat(iterator.hasNext(), is(false));
+ }
+
+ @Test
+ public void testIteratorWithAdditionEmpty() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ Iterator<Entry<Integer, Boolean>> iterator = map.iteratorWithAdditions();
+ assertThat(iterator.hasNext(), is(false));
+ }
+
+ @Test
+ public void testEldest() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ assertThat(map.eldest(), nullValue());
+ map.putIfAbsent(1, false);
+ assertThat(map.eldest().getKey(), is(1));
+ map.putIfAbsent(2, false);
+ assertThat(map.eldest().getKey(), is(1));
+ map.remove(1);
+ assertThat(map.eldest().getKey(), is(2));
+ map.remove(2);
+ assertThat(map.eldest(), nullValue());
+ }
+
+ @Test
+ public void testNewest() {
+ SafeIterableMap<Integer, Boolean> map = mapOf();
+ assertThat(map.newest(), nullValue());
+ map.putIfAbsent(1, false);
+ assertThat(map.newest().getKey(), is(1));
+ map.putIfAbsent(2, false);
+ assertThat(map.newest().getKey(), is(2));
+ map.remove(2);
+ assertThat(map.eldest().getKey(), is(1));
+ map.remove(1);
+ assertThat(map.newest(), nullValue());
+ }
+
+
+ // for most operations we don't care about values, so we create map from key to true
+ @SafeVarargs
+ private static <K> SafeIterableMap<K, Boolean> mapOf(K... keys) {
+ SafeIterableMap<K, Boolean> map = new SafeIterableMap<>();
+ for (K key : keys) {
+ map.putIfAbsent(key, true);
+ }
+ return map;
+ }
+
+ @SafeVarargs
+ private static <K> MapBuilder<K> from(K... keys) {
+ return new MapBuilder<>(keys);
+ }
+
+ private static class MapBuilder<K> {
+ final K[] mKeys;
+
+ MapBuilder(K[] keys) {
+ this.mKeys = keys;
+ }
+
+ @SafeVarargs
+ public final <V> SafeIterableMap<K, V> to(V... values) {
+ assertThat("Failed to build Map", mKeys.length, is(values.length));
+ SafeIterableMap<K, V> map = new SafeIterableMap<>();
+ for (int i = 0; i < mKeys.length; i++) {
+ map.putIfAbsent(mKeys[i], values[i]);
+ }
+ return map;
+ }
+ }
+}
+
+
diff --git a/app-toolkit/core-testing/api/current.txt b/app-toolkit/core-testing/api/current.txt
index f1d206c..37e16fb 100644
--- a/app-toolkit/core-testing/api/current.txt
+++ b/app-toolkit/core-testing/api/current.txt
@@ -1,4 +1,4 @@
-package android.arch.core.executor.testing {
+package androidx.arch.core.executor.testing {
public class CountingTaskExecutorRule extends org.junit.rules.TestWatcher {
ctor public CountingTaskExecutorRule();
diff --git a/app-toolkit/core-testing/api/1.0.0.txt b/app-toolkit/core-testing/api_legacy/1.0.0.txt
similarity index 100%
rename from app-toolkit/core-testing/api/1.0.0.txt
rename to app-toolkit/core-testing/api_legacy/1.0.0.txt
diff --git a/app-toolkit/core-testing/api/1.1.0.txt b/app-toolkit/core-testing/api_legacy/1.1.0.txt
similarity index 100%
rename from app-toolkit/core-testing/api/1.1.0.txt
rename to app-toolkit/core-testing/api_legacy/1.1.0.txt
diff --git a/app-toolkit/core-testing/api/1.0.0.txt b/app-toolkit/core-testing/api_legacy/current.txt
similarity index 100%
copy from app-toolkit/core-testing/api/1.0.0.txt
copy to app-toolkit/core-testing/api_legacy/current.txt
diff --git a/app-toolkit/core-testing/build.gradle b/app-toolkit/core-testing/build.gradle
index 946b18d..32d63c8 100644
--- a/app-toolkit/core-testing/build.gradle
+++ b/app-toolkit/core-testing/build.gradle
@@ -24,7 +24,7 @@
}
dependencies {
- api(project(":arch:runtime"))
+ api(project(":arch:core-runtime"))
api(SUPPORT_ANNOTATIONS)
api(JUNIT)
api(MOCKITO_CORE, libs.exclude_bytebuddy)
diff --git a/app-toolkit/core-testing/src/androidTest/java/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java b/app-toolkit/core-testing/src/androidTest/java/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
deleted file mode 100644
index a6a5b2e..0000000
--- a/app-toolkit/core-testing/src/androidTest/java/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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.core.executor.testing;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-public class CountingTaskExecutorRuleTest {
- private final Semaphore mOnIdleCount = new Semaphore(0);
-
- @Rule
- public CountingTaskExecutorRule mRule = new CountingTaskExecutorRule() {
- @Override
- protected void onIdle() {
- super.onIdle();
- mOnIdleCount.release(1);
- }
- };
-
- @Test
- public void initialIdle() {
- assertThat(mRule.isIdle(), is(true));
- }
-
- @Test
- public void busyIO() throws InterruptedException {
- LatchRunnable task = runOnIO();
- singleTaskTest(task);
- }
-
- @Test
- public void busyMain() throws InterruptedException {
- LatchRunnable task = runOnMain();
- singleTaskTest(task);
- }
-
- @Test
- public void multipleTasks() throws InterruptedException {
- List<LatchRunnable> latches = new ArrayList<>(10);
- for (int i = 0; i < 5; i++) {
- latches.add(runOnIO());
- latches.add(runOnMain());
- }
- assertNotIdle();
- for (int i = 0; i < 9; i++) {
- latches.get(i).start();
- }
- for (int i = 0; i < 9; i++) {
- latches.get(i).await();
- }
- assertNotIdle();
-
- LatchRunnable another = runOnIO();
- latches.get(9).startAndFinish();
- assertNotIdle();
-
- another.startAndFinish();
- assertBecomeIdle();
-
- LatchRunnable oneMore = runOnMain();
-
- assertNotIdle();
-
- oneMore.startAndFinish();
- assertBecomeIdle();
- }
-
- private void assertNotIdle() throws InterruptedException {
- assertThat(mOnIdleCount.tryAcquire(300, TimeUnit.MILLISECONDS), is(false));
- assertThat(mRule.isIdle(), is(false));
- }
-
- private void assertBecomeIdle() throws InterruptedException {
- assertThat(mOnIdleCount.tryAcquire(1, TimeUnit.SECONDS), is(true));
- assertThat(mRule.isIdle(), is(true));
- }
-
- private void singleTaskTest(LatchRunnable task)
- throws InterruptedException {
- assertNotIdle();
- task.startAndFinish();
- assertBecomeIdle();
- }
-
- private LatchRunnable runOnIO() {
- LatchRunnable latchRunnable = new LatchRunnable();
- ArchTaskExecutor.getInstance().executeOnDiskIO(latchRunnable);
- return latchRunnable;
- }
-
- private LatchRunnable runOnMain() {
- LatchRunnable latchRunnable = new LatchRunnable();
- ArchTaskExecutor.getInstance().executeOnMainThread(latchRunnable);
- return latchRunnable;
- }
-
- @Test
- public void drainFailure() throws InterruptedException {
- runOnIO();
- try {
- mRule.drainTasks(300, TimeUnit.MILLISECONDS);
- throw new AssertionError("drain should fail");
- } catch (TimeoutException ignored) {
- }
- }
-
- @Test
- public void drainSuccess() throws TimeoutException, InterruptedException {
- final LatchRunnable task = runOnIO();
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- Thread.sleep(300);
- } catch (InterruptedException ignored) {
- }
- task.start();
- }
- }).start();
- mRule.drainTasks(1, TimeUnit.SECONDS);
- }
-
- private static class LatchRunnable implements Runnable {
- private final CountDownLatch mStart = new CountDownLatch(1);
- private final CountDownLatch mEnd = new CountDownLatch(1);
-
- @Override
- public void run() {
- try {
- mStart.await(10, TimeUnit.SECONDS);
- mEnd.countDown();
- } catch (InterruptedException e) {
- throw new AssertionError(e);
- }
- }
-
- void await() throws InterruptedException {
- mEnd.await(10, TimeUnit.SECONDS);
- }
-
- void start() {
- mStart.countDown();
- }
-
- private void startAndFinish() throws InterruptedException {
- start();
- await();
- }
- }
-}
diff --git a/app-toolkit/core-testing/src/androidTest/java/androidx/arch/core/executor/testing/CountingTaskExecutorRuleTest.java b/app-toolkit/core-testing/src/androidTest/java/androidx/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
new file mode 100644
index 0000000..200e64f
--- /dev/null
+++ b/app-toolkit/core-testing/src/androidTest/java/androidx/arch/core/executor/testing/CountingTaskExecutorRuleTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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 androidx.arch.core.executor.testing;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CountingTaskExecutorRuleTest {
+ private final Semaphore mOnIdleCount = new Semaphore(0);
+
+ @Rule
+ public CountingTaskExecutorRule mRule = new CountingTaskExecutorRule() {
+ @Override
+ protected void onIdle() {
+ super.onIdle();
+ mOnIdleCount.release(1);
+ }
+ };
+
+ @Test
+ public void initialIdle() {
+ assertThat(mRule.isIdle(), is(true));
+ }
+
+ @Test
+ public void busyIO() throws InterruptedException {
+ LatchRunnable task = runOnIO();
+ singleTaskTest(task);
+ }
+
+ @Test
+ public void busyMain() throws InterruptedException {
+ LatchRunnable task = runOnMain();
+ singleTaskTest(task);
+ }
+
+ @Test
+ public void multipleTasks() throws InterruptedException {
+ List<LatchRunnable> latches = new ArrayList<>(10);
+ for (int i = 0; i < 5; i++) {
+ latches.add(runOnIO());
+ latches.add(runOnMain());
+ }
+ assertNotIdle();
+ for (int i = 0; i < 9; i++) {
+ latches.get(i).start();
+ }
+ for (int i = 0; i < 9; i++) {
+ latches.get(i).await();
+ }
+ assertNotIdle();
+
+ LatchRunnable another = runOnIO();
+ latches.get(9).startAndFinish();
+ assertNotIdle();
+
+ another.startAndFinish();
+ assertBecomeIdle();
+
+ LatchRunnable oneMore = runOnMain();
+
+ assertNotIdle();
+
+ oneMore.startAndFinish();
+ assertBecomeIdle();
+ }
+
+ private void assertNotIdle() throws InterruptedException {
+ assertThat(mOnIdleCount.tryAcquire(300, TimeUnit.MILLISECONDS), is(false));
+ assertThat(mRule.isIdle(), is(false));
+ }
+
+ private void assertBecomeIdle() throws InterruptedException {
+ assertThat(mOnIdleCount.tryAcquire(1, TimeUnit.SECONDS), is(true));
+ assertThat(mRule.isIdle(), is(true));
+ }
+
+ private void singleTaskTest(LatchRunnable task)
+ throws InterruptedException {
+ assertNotIdle();
+ task.startAndFinish();
+ assertBecomeIdle();
+ }
+
+ private LatchRunnable runOnIO() {
+ LatchRunnable latchRunnable = new LatchRunnable();
+ ArchTaskExecutor.getInstance().executeOnDiskIO(latchRunnable);
+ return latchRunnable;
+ }
+
+ private LatchRunnable runOnMain() {
+ LatchRunnable latchRunnable = new LatchRunnable();
+ ArchTaskExecutor.getInstance().executeOnMainThread(latchRunnable);
+ return latchRunnable;
+ }
+
+ @Test
+ public void drainFailure() throws InterruptedException {
+ runOnIO();
+ try {
+ mRule.drainTasks(300, TimeUnit.MILLISECONDS);
+ throw new AssertionError("drain should fail");
+ } catch (TimeoutException ignored) {
+ }
+ }
+
+ @Test
+ public void drainSuccess() throws TimeoutException, InterruptedException {
+ final LatchRunnable task = runOnIO();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException ignored) {
+ }
+ task.start();
+ }
+ }).start();
+ mRule.drainTasks(1, TimeUnit.SECONDS);
+ }
+
+ private static class LatchRunnable implements Runnable {
+ private final CountDownLatch mStart = new CountDownLatch(1);
+ private final CountDownLatch mEnd = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ try {
+ mStart.await(10, TimeUnit.SECONDS);
+ mEnd.countDown();
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ void await() throws InterruptedException {
+ mEnd.await(10, TimeUnit.SECONDS);
+ }
+
+ void start() {
+ mStart.countDown();
+ }
+
+ private void startAndFinish() throws InterruptedException {
+ start();
+ await();
+ }
+ }
+}
diff --git a/app-toolkit/core-testing/src/main/AndroidManifest.xml b/app-toolkit/core-testing/src/main/AndroidManifest.xml
index d169e00..f5cee7e 100644
--- a/app-toolkit/core-testing/src/main/AndroidManifest.xml
+++ b/app-toolkit/core-testing/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.core.testing">
+ package="androidx.arch.core.testing">
</manifest>
diff --git a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/JunitTaskExecutorRule.java b/app-toolkit/core-testing/src/main/java/android/arch/core/executor/JunitTaskExecutorRule.java
deleted file mode 100644
index c3366f3..0000000
--- a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/JunitTaskExecutorRule.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.core.executor;
-
-import android.support.annotation.RestrictTo;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.MultipleFailureException;
-import org.junit.runners.model.Statement;
-import org.mockito.Mockito;
-
-import java.util.List;
-
-/**
- * A JUnit rule that swaps the task executor with a more controllable one.
- * Once we have the TaskExecutor API, we should consider making this public (via some test package).
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class JunitTaskExecutorRule implements TestRule {
- private final TaskExecutorWithFakeMainThread mTaskExecutor;
-
- public JunitTaskExecutorRule(int ioThreadCount, boolean spyOnExecutor) {
- if (spyOnExecutor) {
- mTaskExecutor = Mockito.spy(new TaskExecutorWithFakeMainThread(ioThreadCount));
- } else {
- mTaskExecutor = new TaskExecutorWithFakeMainThread(ioThreadCount);
- }
-
- }
-
- private void beforeStart() {
- ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
- }
-
- private void afterFinished() {
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-
- public TaskExecutor getTaskExecutor() {
- return mTaskExecutor;
- }
-
- /**
- * Awaits while all currently posted tasks will be finished
- *
- * @param seconds timeout in seconds
- */
- public void drainTasks(int seconds) throws InterruptedException {
- mTaskExecutor.drainTasks(seconds);
- }
-
- @Override
- public Statement apply(final Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- beforeStart();
- try {
- base.evaluate();
- finishExecutors();
- } catch (Throwable t) {
- throw new RuntimeException(t);
- } finally {
- afterFinished();
- }
- }
- };
- }
-
- private void finishExecutors() throws InterruptedException, MultipleFailureException {
- mTaskExecutor.shutdown(10);
- final List<Throwable> errors = mTaskExecutor.getErrors();
- if (!errors.isEmpty()) {
- throw new MultipleFailureException(errors);
- }
- }
-}
diff --git a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/TaskExecutorWithFakeMainThread.java b/app-toolkit/core-testing/src/main/java/android/arch/core/executor/TaskExecutorWithFakeMainThread.java
deleted file mode 100644
index af0aca4..0000000
--- a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/TaskExecutorWithFakeMainThread.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * 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.core.executor;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A TaskExecutor that has a real thread for main thread operations and can wait for execution etc.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class TaskExecutorWithFakeMainThread extends TaskExecutor {
- private List<Throwable> mCaughtExceptions = Collections.synchronizedList(new ArrayList
- <Throwable>());
-
- private ExecutorService mIOService;
-
- private Thread mMainThread;
- private final int mIOThreadCount;
-
- private ExecutorService mMainThreadService =
- Executors.newSingleThreadExecutor(new ThreadFactory() {
- @Override
- public Thread newThread(@NonNull final Runnable r) {
- mMainThread = new LoggingThread(r);
- return mMainThread;
- }
- });
-
- public TaskExecutorWithFakeMainThread(int ioThreadCount) {
- mIOThreadCount = ioThreadCount;
- mIOService = Executors.newFixedThreadPool(ioThreadCount, new ThreadFactory() {
- @Override
- public Thread newThread(@NonNull Runnable r) {
- return new LoggingThread(r);
- }
- });
- }
-
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- mIOService.execute(runnable);
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- // Tasks in SingleThreadExecutor are guaranteed to execute sequentially,
- // and no more than one task will be active at any given time.
- // So if we call this method from the main thread, new task will be scheduled,
- // which is equivalent to post.
- mMainThreadService.execute(runnable);
- }
-
- @Override
- public boolean isMainThread() {
- return Thread.currentThread() == mMainThread;
- }
-
- List<Throwable> getErrors() {
- return mCaughtExceptions;
- }
-
- @SuppressWarnings("SameParameterValue")
- void shutdown(int timeoutInSeconds) throws InterruptedException {
- mMainThreadService.shutdown();
- mIOService.shutdown();
- mMainThreadService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS);
- mIOService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS);
- }
-
- /**
- * Drains tasks at the given time limit
- * @param seconds Number of seconds to wait
- * @throws InterruptedException
- */
- public void drainTasks(int seconds) throws InterruptedException {
- if (isMainThread()) {
- throw new IllegalStateException();
- }
- final CountDownLatch enterLatch = new CountDownLatch(mIOThreadCount);
- final CountDownLatch exitLatch = new CountDownLatch(1);
- for (int i = 0; i < mIOThreadCount; i++) {
- executeOnDiskIO(new Runnable() {
- @Override
- public void run() {
- enterLatch.countDown();
- try {
- exitLatch.await();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
-
- final CountDownLatch mainLatch = new CountDownLatch(1);
- postToMainThread(new Runnable() {
- @Override
- public void run() {
- mainLatch.countDown();
- }
- });
- if (!enterLatch.await(seconds, TimeUnit.SECONDS)) {
- throw new AssertionError("Could not drain IO tasks in " + seconds
- + " seconds");
- }
- exitLatch.countDown();
- if (!mainLatch.await(seconds, TimeUnit.SECONDS)) {
- throw new AssertionError("Could not drain UI tasks in " + seconds
- + " seconds");
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- class LoggingThread extends Thread {
- LoggingThread(final Runnable target) {
- super(new Runnable() {
- @Override
- public void run() {
- try {
- target.run();
- } catch (Throwable t) {
- mCaughtExceptions.add(t);
- }
- }
- });
- }
- }
-}
diff --git a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/testing/CountingTaskExecutorRule.java b/app-toolkit/core-testing/src/main/java/android/arch/core/executor/testing/CountingTaskExecutorRule.java
deleted file mode 100644
index 77133d5..0000000
--- a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/testing/CountingTaskExecutorRule.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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.core.executor.testing;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.DefaultTaskExecutor;
-import android.os.SystemClock;
-
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
- * different one which counts the tasks as they are start and finish.
- * <p>
- * You can use this rule for your host side tests that use Architecture Components.
- */
-public class CountingTaskExecutorRule extends TestWatcher {
- private final Object mCountLock = new Object();
- private int mTaskCount = 0;
-
- @Override
- protected void starting(Description description) {
- super.starting(description);
- ArchTaskExecutor.getInstance().setDelegate(new DefaultTaskExecutor() {
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- super.executeOnDiskIO(new CountingRunnable(runnable));
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- super.postToMainThread(new CountingRunnable(runnable));
- }
- });
- }
-
- @Override
- protected void finished(Description description) {
- super.finished(description);
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-
- private void increment() {
- synchronized (mCountLock) {
- mTaskCount++;
- }
- }
-
- private void decrement() {
- synchronized (mCountLock) {
- mTaskCount--;
- if (mTaskCount == 0) {
- onIdle();
- mCountLock.notifyAll();
- }
- }
- }
-
- /**
- * Called when the number of awaiting tasks reaches to 0.
- *
- * @see #isIdle()
- */
- protected void onIdle() {
-
- }
-
- /**
- * Returns false if there are tasks waiting to be executed, true otherwise.
- *
- * @return False if there are tasks waiting to be executed, true otherwise.
- *
- * @see #onIdle()
- */
- public boolean isIdle() {
- synchronized (mCountLock) {
- return mTaskCount == 0;
- }
- }
-
- /**
- * Waits until all active tasks are finished.
- *
- * @param time The duration to wait
- * @param timeUnit The time unit for the {@code time} parameter
- *
- * @throws InterruptedException If thread is interrupted while waiting
- * @throws TimeoutException If tasks cannot be drained at the given time
- */
- public void drainTasks(int time, TimeUnit timeUnit)
- throws InterruptedException, TimeoutException {
- long end = SystemClock.uptimeMillis() + timeUnit.toMillis(time);
- synchronized (mCountLock) {
- while (mTaskCount != 0) {
- long now = SystemClock.uptimeMillis();
- long remaining = end - now;
- if (remaining > 0) {
- mCountLock.wait(remaining);
- } else {
- throw new TimeoutException("could not drain tasks");
- }
- }
- }
- }
-
- class CountingRunnable implements Runnable {
- final Runnable mWrapped;
-
- CountingRunnable(Runnable wrapped) {
- mWrapped = wrapped;
- increment();
- }
-
- @Override
- public void run() {
- try {
- mWrapped.run();
- } finally {
- decrement();
- }
- }
- }
-}
diff --git a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/testing/InstantTaskExecutorRule.java b/app-toolkit/core-testing/src/main/java/android/arch/core/executor/testing/InstantTaskExecutorRule.java
deleted file mode 100644
index f88a3e3..0000000
--- a/app-toolkit/core-testing/src/main/java/android/arch/core/executor/testing/InstantTaskExecutorRule.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.core.executor.testing;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.TaskExecutor;
-
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
-/**
- * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
- * different one which executes each task synchronously.
- * <p>
- * You can use this rule for your host side tests that use Architecture Components.
- */
-public class InstantTaskExecutorRule extends TestWatcher {
- @Override
- protected void starting(Description description) {
- super.starting(description);
- ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- runnable.run();
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- runnable.run();
- }
-
- @Override
- public boolean isMainThread() {
- return true;
- }
- });
- }
-
- @Override
- protected void finished(Description description) {
- super.finished(description);
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-}
diff --git a/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/JunitTaskExecutorRule.java b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/JunitTaskExecutorRule.java
new file mode 100644
index 0000000..b02339d
--- /dev/null
+++ b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/JunitTaskExecutorRule.java
@@ -0,0 +1,94 @@
+/*
+ * 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 androidx.arch.core.executor;
+
+import androidx.annotation.RestrictTo;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+/**
+ * A JUnit rule that swaps the task executor with a more controllable one.
+ * Once we have the TaskExecutor API, we should consider making this public (via some test package).
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JunitTaskExecutorRule implements TestRule {
+ private final TaskExecutorWithFakeMainThread mTaskExecutor;
+
+ public JunitTaskExecutorRule(int ioThreadCount, boolean spyOnExecutor) {
+ if (spyOnExecutor) {
+ mTaskExecutor = Mockito.spy(new TaskExecutorWithFakeMainThread(ioThreadCount));
+ } else {
+ mTaskExecutor = new TaskExecutorWithFakeMainThread(ioThreadCount);
+ }
+
+ }
+
+ private void beforeStart() {
+ ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
+ }
+
+ private void afterFinished() {
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+
+ public TaskExecutor getTaskExecutor() {
+ return mTaskExecutor;
+ }
+
+ /**
+ * Awaits while all currently posted tasks will be finished
+ *
+ * @param seconds timeout in seconds
+ */
+ public void drainTasks(int seconds) throws InterruptedException {
+ mTaskExecutor.drainTasks(seconds);
+ }
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ beforeStart();
+ try {
+ base.evaluate();
+ finishExecutors();
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ } finally {
+ afterFinished();
+ }
+ }
+ };
+ }
+
+ private void finishExecutors() throws InterruptedException, MultipleFailureException {
+ mTaskExecutor.shutdown(10);
+ final List<Throwable> errors = mTaskExecutor.getErrors();
+ if (!errors.isEmpty()) {
+ throw new MultipleFailureException(errors);
+ }
+ }
+}
diff --git a/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/TaskExecutorWithFakeMainThread.java b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/TaskExecutorWithFakeMainThread.java
new file mode 100644
index 0000000..a9c1ff6
--- /dev/null
+++ b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/TaskExecutorWithFakeMainThread.java
@@ -0,0 +1,154 @@
+/*
+ * 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 androidx.arch.core.executor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A TaskExecutor that has a real thread for main thread operations and can wait for execution etc.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class TaskExecutorWithFakeMainThread extends TaskExecutor {
+ private List<Throwable> mCaughtExceptions = Collections.synchronizedList(new ArrayList
+ <Throwable>());
+
+ private ExecutorService mIOService;
+
+ private Thread mMainThread;
+ private final int mIOThreadCount;
+
+ private ExecutorService mMainThreadService =
+ Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(@NonNull final Runnable r) {
+ mMainThread = new LoggingThread(r);
+ return mMainThread;
+ }
+ });
+
+ public TaskExecutorWithFakeMainThread(int ioThreadCount) {
+ mIOThreadCount = ioThreadCount;
+ mIOService = Executors.newFixedThreadPool(ioThreadCount, new ThreadFactory() {
+ @Override
+ public Thread newThread(@NonNull Runnable r) {
+ return new LoggingThread(r);
+ }
+ });
+ }
+
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ mIOService.execute(runnable);
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ // Tasks in SingleThreadExecutor are guaranteed to execute sequentially,
+ // and no more than one task will be active at any given time.
+ // So if we call this method from the main thread, new task will be scheduled,
+ // which is equivalent to post.
+ mMainThreadService.execute(runnable);
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return Thread.currentThread() == mMainThread;
+ }
+
+ List<Throwable> getErrors() {
+ return mCaughtExceptions;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ void shutdown(int timeoutInSeconds) throws InterruptedException {
+ mMainThreadService.shutdown();
+ mIOService.shutdown();
+ mMainThreadService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS);
+ mIOService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Drains tasks at the given time limit
+ * @param seconds Number of seconds to wait
+ * @throws InterruptedException
+ */
+ public void drainTasks(int seconds) throws InterruptedException {
+ if (isMainThread()) {
+ throw new IllegalStateException();
+ }
+ final CountDownLatch enterLatch = new CountDownLatch(mIOThreadCount);
+ final CountDownLatch exitLatch = new CountDownLatch(1);
+ for (int i = 0; i < mIOThreadCount; i++) {
+ executeOnDiskIO(new Runnable() {
+ @Override
+ public void run() {
+ enterLatch.countDown();
+ try {
+ exitLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+
+ final CountDownLatch mainLatch = new CountDownLatch(1);
+ postToMainThread(new Runnable() {
+ @Override
+ public void run() {
+ mainLatch.countDown();
+ }
+ });
+ if (!enterLatch.await(seconds, TimeUnit.SECONDS)) {
+ throw new AssertionError("Could not drain IO tasks in " + seconds
+ + " seconds");
+ }
+ exitLatch.countDown();
+ if (!mainLatch.await(seconds, TimeUnit.SECONDS)) {
+ throw new AssertionError("Could not drain UI tasks in " + seconds
+ + " seconds");
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ class LoggingThread extends Thread {
+ LoggingThread(final Runnable target) {
+ super(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ target.run();
+ } catch (Throwable t) {
+ mCaughtExceptions.add(t);
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/testing/CountingTaskExecutorRule.java b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/testing/CountingTaskExecutorRule.java
new file mode 100644
index 0000000..d3c9878
--- /dev/null
+++ b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/testing/CountingTaskExecutorRule.java
@@ -0,0 +1,142 @@
+/*
+ * 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 androidx.arch.core.executor.testing;
+
+import android.os.SystemClock;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.DefaultTaskExecutor;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
+ * different one which counts the tasks as they are start and finish.
+ * <p>
+ * You can use this rule for your host side tests that use Architecture Components.
+ */
+public class CountingTaskExecutorRule extends TestWatcher {
+ private final Object mCountLock = new Object();
+ private int mTaskCount = 0;
+
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ ArchTaskExecutor.getInstance().setDelegate(new DefaultTaskExecutor() {
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ super.executeOnDiskIO(new CountingRunnable(runnable));
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ super.postToMainThread(new CountingRunnable(runnable));
+ }
+ });
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+
+ private void increment() {
+ synchronized (mCountLock) {
+ mTaskCount++;
+ }
+ }
+
+ private void decrement() {
+ synchronized (mCountLock) {
+ mTaskCount--;
+ if (mTaskCount == 0) {
+ onIdle();
+ mCountLock.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Called when the number of awaiting tasks reaches to 0.
+ *
+ * @see #isIdle()
+ */
+ protected void onIdle() {
+
+ }
+
+ /**
+ * Returns false if there are tasks waiting to be executed, true otherwise.
+ *
+ * @return False if there are tasks waiting to be executed, true otherwise.
+ *
+ * @see #onIdle()
+ */
+ public boolean isIdle() {
+ synchronized (mCountLock) {
+ return mTaskCount == 0;
+ }
+ }
+
+ /**
+ * Waits until all active tasks are finished.
+ *
+ * @param time The duration to wait
+ * @param timeUnit The time unit for the {@code time} parameter
+ *
+ * @throws InterruptedException If thread is interrupted while waiting
+ * @throws TimeoutException If tasks cannot be drained at the given time
+ */
+ public void drainTasks(int time, TimeUnit timeUnit)
+ throws InterruptedException, TimeoutException {
+ long end = SystemClock.uptimeMillis() + timeUnit.toMillis(time);
+ synchronized (mCountLock) {
+ while (mTaskCount != 0) {
+ long now = SystemClock.uptimeMillis();
+ long remaining = end - now;
+ if (remaining > 0) {
+ mCountLock.wait(remaining);
+ } else {
+ throw new TimeoutException("could not drain tasks");
+ }
+ }
+ }
+ }
+
+ class CountingRunnable implements Runnable {
+ final Runnable mWrapped;
+
+ CountingRunnable(Runnable wrapped) {
+ mWrapped = wrapped;
+ increment();
+ }
+
+ @Override
+ public void run() {
+ try {
+ mWrapped.run();
+ } finally {
+ decrement();
+ }
+ }
+ }
+}
diff --git a/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/testing/InstantTaskExecutorRule.java b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
new file mode 100644
index 0000000..8c5d0b5
--- /dev/null
+++ b/app-toolkit/core-testing/src/main/java/androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
@@ -0,0 +1,58 @@
+/*
+ * 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 androidx.arch.core.executor.testing;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
+ * different one which executes each task synchronously.
+ * <p>
+ * You can use this rule for your host side tests that use Architecture Components.
+ */
+public class InstantTaskExecutorRule extends TestWatcher {
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return true;
+ }
+ });
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+}
diff --git a/app-toolkit/core-testing/src/test/java/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java b/app-toolkit/core-testing/src/test/java/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
deleted file mode 100644
index 0fdcbfb..0000000
--- a/app-toolkit/core-testing/src/test/java/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.core.executor.testing;
-
-import static org.junit.Assert.assertTrue;
-
-import android.arch.core.executor.ArchTaskExecutor;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(JUnit4.class)
-public class InstantTaskExecutorRuleTest {
- @Rule
- public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
-
- @Test
- public void executeOnMain() throws ExecutionException, InterruptedException, TimeoutException {
- final Thread current = Thread.currentThread();
- FutureTask<Void> check = new FutureTask<>(new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- assertTrue(Thread.currentThread() == current);
- return null;
- }
- });
- ArchTaskExecutor.getInstance().executeOnMainThread(check);
- check.get(1, TimeUnit.SECONDS);
- }
-
- @Test
- public void executeOnIO() throws ExecutionException, InterruptedException, TimeoutException {
- final Thread current = Thread.currentThread();
- FutureTask<Void> check = new FutureTask<>(new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- assertTrue(Thread.currentThread() == current);
- return null;
- }
- });
- ArchTaskExecutor.getInstance().executeOnDiskIO(check);
- check.get(1, TimeUnit.SECONDS);
- }
-}
diff --git a/app-toolkit/core-testing/src/test/java/androidx/arch/core/executor/testing/InstantTaskExecutorRuleTest.java b/app-toolkit/core-testing/src/test/java/androidx/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
new file mode 100644
index 0000000..ecbcfd5
--- /dev/null
+++ b/app-toolkit/core-testing/src/test/java/androidx/arch/core/executor/testing/InstantTaskExecutorRuleTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 androidx.arch.core.executor.testing;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(JUnit4.class)
+public class InstantTaskExecutorRuleTest {
+ @Rule
+ public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Test
+ public void executeOnMain() throws ExecutionException, InterruptedException, TimeoutException {
+ final Thread current = Thread.currentThread();
+ FutureTask<Void> check = new FutureTask<>(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ assertTrue(Thread.currentThread() == current);
+ return null;
+ }
+ });
+ ArchTaskExecutor.getInstance().executeOnMainThread(check);
+ check.get(1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void executeOnIO() throws ExecutionException, InterruptedException, TimeoutException {
+ final Thread current = Thread.currentThread();
+ FutureTask<Void> check = new FutureTask<>(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ assertTrue(Thread.currentThread() == current);
+ return null;
+ }
+ });
+ ArchTaskExecutor.getInstance().executeOnDiskIO(check);
+ check.get(1, TimeUnit.SECONDS);
+ }
+}
diff --git a/app-toolkit/runtime/api/1.0.0.txt b/app-toolkit/runtime/api_legacy/1.0.0.txt
similarity index 100%
rename from app-toolkit/runtime/api/1.0.0.txt
rename to app-toolkit/runtime/api_legacy/1.0.0.txt
diff --git a/app-toolkit/runtime/api/1.1.0.ignore b/app-toolkit/runtime/api_legacy/1.1.0.ignore
similarity index 100%
rename from app-toolkit/runtime/api/1.1.0.ignore
rename to app-toolkit/runtime/api_legacy/1.1.0.ignore
diff --git a/app-toolkit/runtime/api/1.1.0.txt b/app-toolkit/runtime/api_legacy/1.1.0.txt
similarity index 100%
rename from app-toolkit/runtime/api/1.1.0.txt
rename to app-toolkit/runtime/api_legacy/1.1.0.txt
diff --git a/app-toolkit/runtime/api/current.txt b/app-toolkit/runtime/api_legacy/current.txt
similarity index 100%
rename from app-toolkit/runtime/api/current.txt
rename to app-toolkit/runtime/api_legacy/current.txt
diff --git a/app-toolkit/runtime/build.gradle b/app-toolkit/runtime/build.gradle
index 5c8f2d9..0010920 100644
--- a/app-toolkit/runtime/build.gradle
+++ b/app-toolkit/runtime/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api(SUPPORT_ANNOTATIONS)
- api(project(":arch:common"))
+ api(project(":arch:core-common"))
}
supportLibrary {
diff --git a/app-toolkit/runtime/src/main/AndroidManifest.xml b/app-toolkit/runtime/src/main/AndroidManifest.xml
index 3f40068..e56fa1a 100644
--- a/app-toolkit/runtime/src/main/AndroidManifest.xml
+++ b/app-toolkit/runtime/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.core">
+ package="androidx.arch.core">
</manifest>
diff --git a/app-toolkit/runtime/src/main/java/android/arch/core/executor/ArchTaskExecutor.java b/app-toolkit/runtime/src/main/java/android/arch/core/executor/ArchTaskExecutor.java
deleted file mode 100644
index 6276ee3..0000000
--- a/app-toolkit/runtime/src/main/java/android/arch/core/executor/ArchTaskExecutor.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.core.executor;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.concurrent.Executor;
-
-/**
- * A static class that serves as a central point to execute common tasks.
- * <p>
- *
- * @hide This API is not final.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ArchTaskExecutor extends TaskExecutor {
- private static volatile ArchTaskExecutor sInstance;
-
- @NonNull
- private TaskExecutor mDelegate;
-
- @NonNull
- private TaskExecutor mDefaultTaskExecutor;
-
- @NonNull
- private static final Executor sMainThreadExecutor = new Executor() {
- @Override
- public void execute(Runnable command) {
- getInstance().postToMainThread(command);
- }
- };
-
- @NonNull
- private static final Executor sIOThreadExecutor = new Executor() {
- @Override
- public void execute(Runnable command) {
- getInstance().executeOnDiskIO(command);
- }
- };
-
- private ArchTaskExecutor() {
- mDefaultTaskExecutor = new DefaultTaskExecutor();
- mDelegate = mDefaultTaskExecutor;
- }
-
- /**
- * Returns an instance of the task executor.
- *
- * @return The singleton ArchTaskExecutor.
- */
- @NonNull
- public static ArchTaskExecutor getInstance() {
- if (sInstance != null) {
- return sInstance;
- }
- synchronized (ArchTaskExecutor.class) {
- if (sInstance == null) {
- sInstance = new ArchTaskExecutor();
- }
- }
- return sInstance;
- }
-
- /**
- * Sets a delegate to handle task execution requests.
- * <p>
- * If you have a common executor, you can set it as the delegate and App Toolkit components will
- * use your executors. You may also want to use this for your tests.
- * <p>
- * Calling this method with {@code null} sets it to the default TaskExecutor.
- *
- * @param taskExecutor The task executor to handle task requests.
- */
- public void setDelegate(@Nullable TaskExecutor taskExecutor) {
- mDelegate = taskExecutor == null ? mDefaultTaskExecutor : taskExecutor;
- }
-
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- mDelegate.executeOnDiskIO(runnable);
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- mDelegate.postToMainThread(runnable);
- }
-
- @NonNull
- public static Executor getMainThreadExecutor() {
- return sMainThreadExecutor;
- }
-
- @NonNull
- public static Executor getIOThreadExecutor() {
- return sIOThreadExecutor;
- }
-
- @Override
- public boolean isMainThread() {
- return mDelegate.isMainThread();
- }
-}
diff --git a/app-toolkit/runtime/src/main/java/android/arch/core/executor/DefaultTaskExecutor.java b/app-toolkit/runtime/src/main/java/android/arch/core/executor/DefaultTaskExecutor.java
deleted file mode 100644
index dbb1054..0000000
--- a/app-toolkit/runtime/src/main/java/android/arch/core/executor/DefaultTaskExecutor.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.core.executor;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class DefaultTaskExecutor extends TaskExecutor {
- private final Object mLock = new Object();
- private ExecutorService mDiskIO = Executors.newFixedThreadPool(2);
-
- @Nullable
- private volatile Handler mMainHandler;
-
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- mDiskIO.execute(runnable);
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- if (mMainHandler == null) {
- synchronized (mLock) {
- if (mMainHandler == null) {
- mMainHandler = new Handler(Looper.getMainLooper());
- }
- }
- }
- //noinspection ConstantConditions
- mMainHandler.post(runnable);
- }
-
- @Override
- public boolean isMainThread() {
- return Looper.getMainLooper().getThread() == Thread.currentThread();
- }
-}
diff --git a/app-toolkit/runtime/src/main/java/android/arch/core/executor/TaskExecutor.java b/app-toolkit/runtime/src/main/java/android/arch/core/executor/TaskExecutor.java
deleted file mode 100644
index 7175801..0000000
--- a/app-toolkit/runtime/src/main/java/android/arch/core/executor/TaskExecutor.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.core.executor;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-
-/**
- * A task executor that can divide tasks into logical groups.
- * <p>
- * It holds a collection a executors for each group of task.
- * <p>
- * TODO: Don't use this from outside, we don't know what the API will look like yet.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class TaskExecutor {
- /**
- * Executes the given task in the disk IO thread pool.
- *
- * @param runnable The runnable to run in the disk IO thread pool.
- */
- public abstract void executeOnDiskIO(@NonNull Runnable runnable);
-
- /**
- * Posts the given task to the main thread.
- *
- * @param runnable The runnable to run on the main thread.
- */
- public abstract void postToMainThread(@NonNull Runnable runnable);
-
- /**
- * Executes the given task on the main thread.
- * <p>
- * If the current thread is a main thread, immediately runs the given runnable.
- *
- * @param runnable The runnable to run on the main thread.
- */
- public void executeOnMainThread(@NonNull Runnable runnable) {
- if (isMainThread()) {
- runnable.run();
- } else {
- postToMainThread(runnable);
- }
- }
-
- /**
- * Returns true if the current thread is the main thread, false otherwise.
- *
- * @return true if we are on the main thread, false otherwise.
- */
- public abstract boolean isMainThread();
-}
diff --git a/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/ArchTaskExecutor.java b/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/ArchTaskExecutor.java
new file mode 100644
index 0000000..a38dff8
--- /dev/null
+++ b/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/ArchTaskExecutor.java
@@ -0,0 +1,118 @@
+/*
+ * 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 androidx.arch.core.executor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A static class that serves as a central point to execute common tasks.
+ * <p>
+ *
+ * @hide This API is not final.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ArchTaskExecutor extends TaskExecutor {
+ private static volatile ArchTaskExecutor sInstance;
+
+ @NonNull
+ private TaskExecutor mDelegate;
+
+ @NonNull
+ private TaskExecutor mDefaultTaskExecutor;
+
+ @NonNull
+ private static final Executor sMainThreadExecutor = new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ getInstance().postToMainThread(command);
+ }
+ };
+
+ @NonNull
+ private static final Executor sIOThreadExecutor = new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ getInstance().executeOnDiskIO(command);
+ }
+ };
+
+ private ArchTaskExecutor() {
+ mDefaultTaskExecutor = new DefaultTaskExecutor();
+ mDelegate = mDefaultTaskExecutor;
+ }
+
+ /**
+ * Returns an instance of the task executor.
+ *
+ * @return The singleton ArchTaskExecutor.
+ */
+ @NonNull
+ public static ArchTaskExecutor getInstance() {
+ if (sInstance != null) {
+ return sInstance;
+ }
+ synchronized (ArchTaskExecutor.class) {
+ if (sInstance == null) {
+ sInstance = new ArchTaskExecutor();
+ }
+ }
+ return sInstance;
+ }
+
+ /**
+ * Sets a delegate to handle task execution requests.
+ * <p>
+ * If you have a common executor, you can set it as the delegate and App Toolkit components will
+ * use your executors. You may also want to use this for your tests.
+ * <p>
+ * Calling this method with {@code null} sets it to the default TaskExecutor.
+ *
+ * @param taskExecutor The task executor to handle task requests.
+ */
+ public void setDelegate(@Nullable TaskExecutor taskExecutor) {
+ mDelegate = taskExecutor == null ? mDefaultTaskExecutor : taskExecutor;
+ }
+
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ mDelegate.executeOnDiskIO(runnable);
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ mDelegate.postToMainThread(runnable);
+ }
+
+ @NonNull
+ public static Executor getMainThreadExecutor() {
+ return sMainThreadExecutor;
+ }
+
+ @NonNull
+ public static Executor getIOThreadExecutor() {
+ return sIOThreadExecutor;
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return mDelegate.isMainThread();
+ }
+}
diff --git a/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java b/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java
new file mode 100644
index 0000000..a942324
--- /dev/null
+++ b/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java
@@ -0,0 +1,61 @@
+/*
+ * 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 androidx.arch.core.executor;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class DefaultTaskExecutor extends TaskExecutor {
+ private final Object mLock = new Object();
+ private ExecutorService mDiskIO = Executors.newFixedThreadPool(2);
+
+ @Nullable
+ private volatile Handler mMainHandler;
+
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ mDiskIO.execute(runnable);
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ if (mMainHandler == null) {
+ synchronized (mLock) {
+ if (mMainHandler == null) {
+ mMainHandler = new Handler(Looper.getMainLooper());
+ }
+ }
+ }
+ //noinspection ConstantConditions
+ mMainHandler.post(runnable);
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return Looper.getMainLooper().getThread() == Thread.currentThread();
+ }
+}
diff --git a/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/TaskExecutor.java b/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/TaskExecutor.java
new file mode 100644
index 0000000..4b3c5fa
--- /dev/null
+++ b/app-toolkit/runtime/src/main/java/androidx/arch/core/executor/TaskExecutor.java
@@ -0,0 +1,67 @@
+/*
+ * 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 androidx.arch.core.executor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * A task executor that can divide tasks into logical groups.
+ * <p>
+ * It holds a collection a executors for each group of task.
+ * <p>
+ * TODO: Don't use this from outside, we don't know what the API will look like yet.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class TaskExecutor {
+ /**
+ * Executes the given task in the disk IO thread pool.
+ *
+ * @param runnable The runnable to run in the disk IO thread pool.
+ */
+ public abstract void executeOnDiskIO(@NonNull Runnable runnable);
+
+ /**
+ * Posts the given task to the main thread.
+ *
+ * @param runnable The runnable to run on the main thread.
+ */
+ public abstract void postToMainThread(@NonNull Runnable runnable);
+
+ /**
+ * Executes the given task on the main thread.
+ * <p>
+ * If the current thread is a main thread, immediately runs the given runnable.
+ *
+ * @param runnable The runnable to run on the main thread.
+ */
+ public void executeOnMainThread(@NonNull Runnable runnable) {
+ if (isMainThread()) {
+ runnable.run();
+ } else {
+ postToMainThread(runnable);
+ }
+ }
+
+ /**
+ * Returns true if the current thread is the main thread, false otherwise.
+ *
+ * @return true if we are on the main thread, false otherwise.
+ */
+ public abstract boolean isMainThread();
+}
diff --git a/app-toolkit/settings.gradle b/app-toolkit/settings.gradle
index 9bb671a..b463df7 100644
--- a/app-toolkit/settings.gradle
+++ b/app-toolkit/settings.gradle
@@ -48,33 +48,38 @@
println "support root:${supportRoot}"
-includeProject(":arch:common", new File(supportRoot, "app-toolkit/common"))
+includeProject(":arch:core-common", new File(supportRoot, "app-toolkit/common"))
includeProject(":arch:core-testing", new File(supportRoot, "app-toolkit/core-testing"))
-includeProject(":arch:runtime", new File(supportRoot, "app-toolkit/runtime"))
-includeProject(":lifecycle:common", new File(supportRoot, "lifecycle/common"))
-includeProject(":lifecycle:common-java8", new File(supportRoot, "lifecycle/common-java8"))
-includeProject(":lifecycle:compiler", new File(supportRoot, "lifecycle/compiler"))
-includeProject(":lifecycle:extensions", new File(supportRoot, "lifecycle/extensions"))
+includeProject(":arch:core-runtime", new File(supportRoot, "app-toolkit/runtime"))
includeProject(":lifecycle:integration-tests:testapp", new File(supportRoot, "lifecycle/integration-tests/testapp"))
-includeProject(":lifecycle:livedata-core", new File(supportRoot, "lifecycle/livedata-core"))
-includeProject(":lifecycle:livedata", new File(supportRoot, "lifecycle/livedata"))
-includeProject(":lifecycle:reactivestreams", new File(supportRoot, "lifecycle/reactivestreams"))
-includeProject(":lifecycle:runtime", new File(supportRoot, "lifecycle/runtime"))
-includeProject(":lifecycle:viewmodel", new File(supportRoot, "lifecycle/viewmodel"))
-includeProject(":paging:common", new File(supportRoot, "paging/common"))
+includeProject(":lifecycle:lifecycle-common", new File(supportRoot, "lifecycle/common"))
+includeProject(":lifecycle:lifecycle-common-java8", new File(supportRoot, "lifecycle/common-java8"))
+includeProject(":lifecycle:lifecycle-compiler", new File(supportRoot, "lifecycle/compiler"))
+includeProject(":lifecycle:lifecycle-extensions", new File(supportRoot, "lifecycle/extensions"))
+includeProject(":lifecycle:lifecycle-livedata-core", new File(supportRoot, "lifecycle/livedata-core"))
+includeProject(":lifecycle:lifecycle-livedata", new File(supportRoot, "lifecycle/livedata"))
+includeProject(":lifecycle:lifecycle-reactivestreams", new File(supportRoot, "lifecycle/reactivestreams"))
+includeProject(":lifecycle:lifecycle-runtime", new File(supportRoot, "lifecycle/runtime"))
+includeProject(":lifecycle:lifecycle-viewmodel", new File(supportRoot, "lifecycle/viewmodel"))
includeProject(":paging:integration-tests:testapp", new File(supportRoot, "paging/integration-tests/testapp"))
-includeProject(":paging:runtime", new File(supportRoot, "paging/runtime"))
-includeProject(":persistence:db", new File(supportRoot, "persistence/db"))
-includeProject(":persistence:db-framework", new File(supportRoot, "persistence/db-framework"))
-includeProject(":room:common", new File(supportRoot, "room/common"))
-includeProject(":room:compiler", new File(supportRoot, "room/compiler"))
-includeProject(":room:guava", new File(supportRoot, "room/guava"))
+includeProject(":paging:paging-common", new File(supportRoot, "paging/common"))
+includeProject(":paging:paging-runtime", new File(supportRoot, "paging/runtime"))
includeProject(":room:integration-tests:testapp", new File(supportRoot, "room/integration-tests/testapp"))
includeProject(":room:integration-tests:kotlintestapp", new File(supportRoot, "room/integration-tests/kotlintestapp"))
-includeProject(":room:migration", new File(supportRoot, "room/migration"))
-includeProject(":room:runtime", new File(supportRoot, "room/runtime"))
-includeProject(":room:rxjava2", new File(supportRoot, "room/rxjava2"))
-includeProject(":room:testing", new File(supportRoot, "room/testing"))
+includeProject(":room:room-common", new File(supportRoot, "room/common"))
+includeProject(":room:room-compiler", new File(supportRoot, "room/compiler"))
+includeProject(":room:room-guava", new File(supportRoot, "room/guava"))
+includeProject(":room:room-migration", new File(supportRoot, "room/migration"))
+includeProject(":room:room-runtime", new File(supportRoot, "room/runtime"))
+includeProject(":room:room-rxjava2", new File(supportRoot, "room/rxjava2"))
+includeProject(":room:room-testing", new File(supportRoot, "room/testing"))
+includeProject(":sqlite:sqlite", new File(supportRoot, "persistence/db"))
+includeProject(":sqlite:sqlite-framework", new File(supportRoot, "persistence/db-framework"))
+
+includeProject(":jetifier-core", new File(supportRoot, "jetifier/jetifier/core"))
+includeProject(":jetifier-gradle-plugin", new File(supportRoot, "jetifier/jetifier/gradle-plugin"))
+includeProject(":jetifier-standalone", new File(supportRoot, "jetifier/jetifier/standalone"))
+includeProject(":jetifier-preprocessor", new File(supportRoot, "jetifier/jetifier/preprocessor"))
/////////////////////////////
//
diff --git a/browser/build.gradle b/browser/build.gradle
index 9930bbd..4cc89d9 100644
--- a/browser/build.gradle
+++ b/browser/build.gradle
@@ -12,7 +12,6 @@
api(project(":interpolator"))
api(project(":collection"))
api(project(":legacy-support-core-ui"))
- api(project(":appcompat"))
androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
androidTestImplementation(ESPRESSO_CORE_TMP, libs.exclude_for_espresso)
diff --git a/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java b/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java
index c34c38f..09efc9c 100644
--- a/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java
+++ b/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java
@@ -39,7 +39,7 @@
private final View mContentView;
BrowserActionsFallbackMenuDialog(Context context, View contentView) {
- super(context, androidx.browser.R.style.Theme_AppCompat_Light_Dialog);
+ super(context);
mContentView = contentView;
}
diff --git a/build.gradle b/build.gradle
index 03b1de7..c731c92 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,7 +36,6 @@
classpath build_libs.gradle
classpath build_libs.lint
classpath build_libs.jacoco
- classpath build_libs.jetifier
classpath build_libs.kotlin.gradle_plugin
}
}
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index ac7c501..4e23e62 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -45,7 +45,6 @@
build_libs.error_prone_gradle = 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.13'
build_libs.jacoco = 'org.jacoco:org.jacoco.core:0.7.8'
build_libs.jacoco_ant = 'org.jacoco:org.jacoco.ant:0.7.8'
-build_libs.jetifier = 'com.android.support.jetifier:jetifier-gradle-plugin:0.2.0'
build_libs.kotlin = [
gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${build_versions.kotlin}"
]
diff --git a/buildSrc/dependencies.gradle b/buildSrc/dependencies.gradle
index 50b31a6..2608258 100644
--- a/buildSrc/dependencies.gradle
+++ b/buildSrc/dependencies.gradle
@@ -39,7 +39,7 @@
libs.support_exclude_config = {
exclude group: 'android.arch.core'
- exclude group: 'android.arch.lifecycle'
+ exclude group: 'androidx.lifecycle'
}
rootProject.ext['libs'] = libs
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index d500eec..df9fc85 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -157,29 +157,22 @@
}
def configureBuildOnServer() {
- def preBuildOnServerTask = rootProject.tasks.create("preBuildOnServer")
def buildOnServerTask = rootProject.tasks.create("buildOnServer")
rootProject.tasks.whenTaskAdded { task ->
if ("createArchive".equals(task.name)
|| "distDocs".equals(task.name)
|| CheckExternalDependencyLicensesTask.ROOT_TASK_NAME.equals(task.name)) {
- preBuildOnServerTask.dependsOn task
+ buildOnServerTask.dependsOn task
}
}
-
subprojects {
project.tasks.whenTaskAdded { task ->
- if ("assembleErrorProne".equals(task.name)) {
- preBuildOnServerTask.dependsOn task
- }
- if ("assembleAndroidTest".equals(task.name)) {
+ if ("assembleErrorProne".equals(task.name) || "assembleAndroidTest".equals(task.name)) {
buildOnServerTask.dependsOn task
- task.dependsOn preBuildOnServerTask
}
}
}
- preBuildOnServerTask.dependsOn createJacocoAntUberJarTask()
- buildOnServerTask.dependsOn preBuildOnServerTask
+ buildOnServerTask.dependsOn createJacocoAntUberJarTask()
}
def createJacocoAntUberJarTask() {
diff --git a/buildSrc/jetify.gradle b/buildSrc/jetify.gradle
index 42df73f..e81d325 100644
--- a/buildSrc/jetify.gradle
+++ b/buildSrc/jetify.gradle
@@ -25,7 +25,7 @@
dependsOn ':jetifier-standalone:installDist'
inputs.file project.tasks['createArchive'].archivePath
- outputs.file "${buildDir}/top-of-tree-m2-repository-dejetified-${project.ext.buildNumber}.zip"
+ outputs.file "${rootProject.ext.distDir}/top-of-tree-m2-repository-dejetified-${project.ext.buildNumber}.zip"
- commandLine ("${jetifierBin}", "-s", "-r", "-outputfile", "${outputs.files.singleFile}", "-i", "${inputs.files.singleFile}", "-l", "error")
+ commandLine ("${jetifierBin}", "-s", "-r", "-outputfile", "${outputs.files.singleFile}", "-i", "${inputs.files.singleFile}", "-l", "error", "-rebuildTopOfTree")
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
index 9b34de7..4a791cc 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
@@ -24,7 +24,7 @@
const val APPCOMPAT = "androidx.appcompat"
const val ASYNCLAYOUTINFLATER = "androidx.asynclayoutinflater"
const val BROWSER = "androidx.browser"
- const val CAR = "androidx.cardview"
+ const val CAR = "androidx.car"
const val CARDVIEW = "androidx.cardview"
const val COLLECTION = "androidx.collection"
const val CONTENTPAGING = "androidx.contentpaging"
@@ -64,11 +64,11 @@
const val VIEWPAGER2 = "androidx.viewpager2"
const val WEAR = "androidx.wear"
const val WEBKIT = "androidx.webkit"
- const val ROOM = "android.arch.persistence.room"
- const val PERSISTENCE = "android.arch.persistence"
- const val LIFECYCLE = "android.arch.lifecycle"
- const val ARCH_CORE = "android.arch.core"
- const val PAGING = "android.arch.paging"
- const val NAVIGATION = "android.arch.navigation"
+ const val ROOM = "androidx.room"
+ const val PERSISTENCE = "androidx.sqlite"
+ const val LIFECYCLE = "androidx.lifecycle"
+ const val ARCH_CORE = "androidx.arch.core"
+ const val PAGING = "androidx.paging"
+ const val NAVIGATION = "androidx.navigation"
const val JETIFIER = "com.android.support.jetifier"
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 90c3ba0..e2993e9 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -28,12 +28,12 @@
/**
* Version code for Room
*/
- val ROOM = Version("1.1.0-alpha3")
+ val ROOM = Version("2.0.0-SNAPSHOT")
/**
* Version code for Lifecycle extensions (ProcessLifecycleOwner, Fragment support)
*/
- val LIFECYCLES_EXT = Version("1.2.0-alpha1")
+ val LIFECYCLES_EXT = Version("2.0.0-SNAPSHOT")
/**
* Version code for Lifecycle LiveData
@@ -48,9 +48,9 @@
/**
* Version code for Paging
*/
- val PAGING = Version("1.0.0-alpha6")
+ val PAGING = Version("2.0.0-SNAPSHOT")
- private val LIFECYCLES = Version("1.1.1-alpha1")
+ private val LIFECYCLES = Version("2.0.0-SNAPSHOT")
/**
* Version code for Lifecycle libs that are required by the support library
@@ -65,7 +65,7 @@
/**
* Version code for shared code of flatfoot
*/
- val ARCH_CORE = Version("1.1.1-alpha1")
+ val ARCH_CORE = Version("2.0.0-SNAPSHOT")
/**
* Version code for shared code of flatfoot runtime
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportAndroidLibraryPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/SupportAndroidLibraryPlugin.kt
index 2843aaf..0d3fa3a 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportAndroidLibraryPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportAndroidLibraryPlugin.kt
@@ -94,6 +94,8 @@
configuration.resolutionStrategy.dependencySubstitution.apply {
substitute(module("com.android.support:support-annotations"))
.with(project(":annotation"))
+ substitute(module("androidx.annotation:annotation"))
+ .with(project(":annotation"))
}
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 3c4ae5c..6fea40e 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -35,19 +35,13 @@
const val KOTLIN_METADATA = "me.eugeniomarletti:kotlin-metadata:1.2.1"
const val LINT = "com.android.tools.lint:lint:26.0.0"
const val MOCKITO_CORE = "org.mockito:mockito-core:2.7.6"
-const val MULTIDEX = "com.android.support:multidex:1.0.1"
+const val MULTIDEX = "androidx.multidex:multidex:2.0.0"
const val NULLAWAY = "com.uber.nullaway:nullaway:0.2.0"
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.0.6"
const val TEST_RUNNER = "com.android.support.test:runner:1.0.1"
const val TEST_RULES = "com.android.support.test:rules:1.0.1"
-
-const val ESPRESSO_CONTRIB_TMP = "com.android.temp.support.test.espresso:espresso-contrib:3.0.1"
-const val ESPRESSO_CORE_TMP = "com.android.temp.support.test.espresso:espresso-core:3.0.1"
-const val TEST_RUNNER_TMP = "com.android.temp.support.test:runner:1.0.1"
-const val TEST_RULES_TMP = "com.android.temp.support.test:rules:1.0.1"
-
-
+const val TRUTH = "com.google.truth:truth:0.34"
/**
* this Xerial version is newer than we want but we need it to fix
* https://github.com/xerial/sqlite-jdbc/issues/97
@@ -55,26 +49,24 @@
*/
const val XERIAL = "org.xerial:sqlite-jdbc:3.20.1"
-// Support library dependencies needed for projects that compile against prebuilt versions
-// instead of source directly.
-// NOTE: _27 versions exist for modules that have opted-in to 27, and tests that depend on those
-// modules. Other projects may stick to older versions to avoid forcing users to update.
-private const val SUPPORT_VERSION_27 = "27.1.0"
-const val SUPPORT_RECYCLERVIEW_27 = "com.android.support:recyclerview-v7:$SUPPORT_VERSION_27"
-const val SUPPORT_APPCOMPAT_27 = "com.android.support:appcompat-v7:$SUPPORT_VERSION_27"
+const val ESPRESSO_CONTRIB_TMP = "com.android.temp.support.test.espresso:espresso-contrib:3.0.1"
+const val ESPRESSO_CORE_TMP = "com.android.temp.support.test.espresso:espresso-core:3.0.1"
+const val TEST_RUNNER_TMP = "com.android.temp.support.test:runner:1.0.1"
+const val TEST_RULES_TMP = "com.android.temp.support.test:rules:1.0.1"
-private const val SUPPORT_VERSION = "26.1.0"
-const val SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$SUPPORT_VERSION"
-const val SUPPORT_APPCOMPAT = "com.android.support:appcompat-v7:$SUPPORT_VERSION"
-const val SUPPORT_CARDVIEW = "com.android.support:cardview-v7:$SUPPORT_VERSION"
-const val SUPPORT_CORE_UTILS = "com.android.support:support-core-utils:$SUPPORT_VERSION"
-const val SUPPORT_DESIGN = "com.android.support:design:$SUPPORT_VERSION"
-const val SUPPORT_FRAGMENTS = "com.android.support:support-fragment:$SUPPORT_VERSION"
-const val SUPPORT_RECYCLERVIEW = "com.android.support:recyclerview-v7:$SUPPORT_VERSION"
-const val SUPPORT_V4 = "com.android.support:support-v4:$SUPPORT_VERSION"
+// AndroidX libraries
+private const val SUPPORT_VERSION = "1.0.0-SNAPSHOT"
+const val SUPPORT_ANNOTATIONS = "androidx.annotation:annotation:$SUPPORT_VERSION"
+const val SUPPORT_APPCOMPAT = "androidx.appcompat:appcompat:$SUPPORT_VERSION"
+const val SUPPORT_CARDVIEW = "androidx.cardview:cardview:$SUPPORT_VERSION"
+const val SUPPORT_CORE_UTILS = "androidx.legacy:legacy-support-core-utils:$SUPPORT_VERSION"
+const val SUPPORT_DESIGN = "com.google.android.material:material:$SUPPORT_VERSION@aar"
+const val SUPPORT_FRAGMENTS = "androidx.fragment:fragment:$SUPPORT_VERSION"
+const val SUPPORT_RECYCLERVIEW = "androidx.recyclerview:recyclerview:$SUPPORT_VERSION"
+const val SUPPORT_V4 = "androidx.legacy:legacy-support-v4:$SUPPORT_VERSION"
// Arch libraries
-const val ARCH_LIFECYCLE_RUNTIME = "androidx.temp.arch.lifecycle:runtime:2.0.0-alpha1@aar"
-const val ARCH_LIFECYCLE_LIVEDATA_CORE = "androidx.temp.arch.lifecycle:livedata-core:2.0.0-alpha1@aar"
-const val ARCH_LIFECYCLE_VIEWMODEL = "androidx.temp.arch.lifecycle:viewmodel:2.0.0-alpha1@aar"
-const val ARCH_LIFECYCLE_EXTENSIONS = "androidx.temp.arch.lifecycle:extensions:2.0.0-alpha1@aar"
+const val ARCH_LIFECYCLE_RUNTIME = "androidx.lifecycle:lifecycle-runtime:2.0.0-SNAPSHOT@aar"
+const val ARCH_LIFECYCLE_LIVEDATA_CORE = "androidx.lifecycle:lifecycle-livedata-core:2.0.0-SNAPSHOT@aar"
+const val ARCH_LIFECYCLE_VIEWMODEL = "androidx.lifecycle:lifecycle-viewmodel:2.0.0-SNAPSHOT@aar"
+const val ARCH_LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:2.0.0-SNAPSHOT@aar"
diff --git a/buildSrc/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt b/buildSrc/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
index 2dc4e01..426df2d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
@@ -50,6 +50,9 @@
.filterNot {
it.group?.startsWith("android.arch") == true
}
+ .filterNot {
+ it.group?.startsWith("androidx") == true
+ }
}
.forEach {
checkerConfig.dependencies.add(it)
diff --git a/car/build.gradle b/car/build.gradle
index 46621f9..d971e78 100644
--- a/car/build.gradle
+++ b/car/build.gradle
@@ -4,7 +4,6 @@
plugins {
id("SupportAndroidLibraryPlugin")
- id("androidx.tools.jetifier")
}
dependencies {
diff --git a/car/car-stubs/android.car.jar b/car/car-stubs/android.car.jar
index 2c8bf77..b7ee974 100644
--- a/car/car-stubs/android.car.jar
+++ b/car/car-stubs/android.car.jar
Binary files differ
diff --git a/car/res/drawable/car_card_background.xml b/car/res/drawable/car_card_background.xml
index 7caa2ff..1a093e3 100644
--- a/car/res/drawable/car_card_background.xml
+++ b/car/res/drawable/car_card_background.xml
@@ -15,5 +15,5 @@
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@color/car_card"/>
+ <solid android:color="?attr/listItemBackgroundColor"/>
</shape>
\ No newline at end of file
diff --git a/car/res/drawable/car_card_rounded_background.xml b/car/res/drawable/car_card_rounded_background.xml
index 594705b..240c38b 100644
--- a/car/res/drawable/car_card_rounded_background.xml
+++ b/car/res/drawable/car_card_rounded_background.xml
@@ -15,7 +15,7 @@
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@color/car_card"/>
+ <solid android:color="?attr/listItemBackgroundColor"/>
<corners
android:radius="@dimen/car_radius_3"/>
</shape>
\ No newline at end of file
diff --git a/car/res/drawable/car_card_rounded_bottom_background.xml b/car/res/drawable/car_card_rounded_bottom_background.xml
index 35dba13..611e032 100644
--- a/car/res/drawable/car_card_rounded_bottom_background.xml
+++ b/car/res/drawable/car_card_rounded_bottom_background.xml
@@ -15,7 +15,7 @@
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@color/car_card"/>
+ <solid android:color="?attr/listItemBackgroundColor"/>
<corners
android:bottomRightRadius="@dimen/car_radius_3"
android:bottomLeftRadius="@dimen/car_radius_3"/>
diff --git a/car/res/drawable/car_card_rounded_top_background.xml b/car/res/drawable/car_card_rounded_top_background.xml
index dfb5622..7e85884 100644
--- a/car/res/drawable/car_card_rounded_top_background.xml
+++ b/car/res/drawable/car_card_rounded_top_background.xml
@@ -15,7 +15,7 @@
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@color/car_card"/>
+ <solid android:color="?attr/listItemBackgroundColor"/>
<corners
android:topRightRadius="@dimen/car_radius_3"
android:topLeftRadius="@dimen/car_radius_3"/>
diff --git a/car/res/values/attrs.xml b/car/res/values/attrs.xml
index 3a2959f..0548f79 100644
--- a/car/res/values/attrs.xml
+++ b/car/res/values/attrs.xml
@@ -162,9 +162,6 @@
<!-- An attribute for specifying a style that modifies the look of a PagedListView. -->
<attr name="pagedListViewStyle" format="reference"/>
- <!-- An attribute for specifying a style that modifies the look of ListItems. -->
- <attr name="listItemStyle" format="reference"/>
-
<!-- ================= -->
<!-- Dialog Attributes -->
<!-- ================= -->
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
index 86bcd2e..bc3880d 100644
--- a/car/res/values/themes.xml
+++ b/car/res/values/themes.xml
@@ -33,7 +33,9 @@
<item name="android:colorControlNormal">@color/car_body2</item>
<item name="carDialogTheme">@style/Theme.Car.Dialog</item>
<item name="pagedListViewStyle">@style/Widget.Car.List</item>
- <item name="listItemStyle">@style/Widget.Car.ListItem</item>
+ <item name="listItemBackgroundColor">@color/car_card</item>
+ <item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1</item>
+ <item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2</item>
</style>
<!-- Theme for the Car that is a passthrough for the default theme. -->
@@ -46,7 +48,9 @@
<item name="alertDialogTheme">@style/Theme.Car.Dark.Dialog.Alert</item>
<item name="carDialogTheme">@style/Theme.Car.Dark.Dialog</item>
<item name="pagedListViewStyle">@style/Widget.Car.Light.List.LightDivider</item>
- <item name="listItemStyle">@style/Widget.Car.ListItem.Dark</item>
+ <item name="listItemBackgroundColor">@color/car_card_dark</item>
+ <item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1.Light</item>
+ <item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2.Light</item>
</style>
<!-- A Theme for activities that have a drawer affordance. This theme will automatically switch
@@ -146,7 +150,9 @@
colors will darken during night mode. -->
<style name="Theme.Car.List" parent="android:Theme">
<item name="pagedListViewStyle">@style/Widget.Car.List</item>
- <item name="listItemStyle">@style/Widget.Car.ListItem</item>
+ <item name="listItemBackgroundColor">@color/car_card</item>
+ <item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1</item>
+ <item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2</item>
</style>
<!-- A theme for PagedListViews that will have a light scrollbars and light-colored items. The
@@ -158,6 +164,8 @@
<!-- A theme for PagedListViews that will have a light scrollbars and dark-colored items. The
colors do not change for night mode. -->
<style name="Theme.Car.Light.List.DarkItems">
- <item name="listItemStyle">@style/Widget.Car.ListItem.Dark</item>
+ <item name="listItemBackgroundColor">@color/car_card_dark</item>
+ <item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1.Light</item>
+ <item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2.Light</item>
</style>
</resources>
diff --git a/car/src/main/java/androidx/car/widget/ListItemAdapter.java b/car/src/main/java/androidx/car/widget/ListItemAdapter.java
index c4496cb..4d58488 100644
--- a/car/src/main/java/androidx/car/widget/ListItemAdapter.java
+++ b/car/src/main/java/androidx/car/widget/ListItemAdapter.java
@@ -23,7 +23,6 @@
import android.os.Bundle;
import android.util.SparseArray;
import android.util.SparseIntArray;
-import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -33,6 +32,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.function.Function;
+import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.LayoutRes;
import androidx.annotation.StyleRes;
@@ -96,7 +96,8 @@
new SparseArray<>();
@ListBackgroundStyle private int mBackgroundStyle;
- @StyleRes private int mListItemStyle;
+
+ @ColorInt private int mListItemBackgroundColor;
@StyleRes private int mListItemTitleTextAppearance;
@StyleRes private int mListItemBodyTextAppearance;
@@ -180,9 +181,11 @@
// When attached to the RecyclerView, update the Context so that this ListItemAdapter can
// retrieve theme information off that view.
mContext = recyclerView.getContext();
- mListItemStyle = getListItemStyle(mContext);
- TypedArray a = mContext.obtainStyledAttributes(mListItemStyle, R.styleable.ListItem);
+ TypedArray a = mContext.getTheme().obtainStyledAttributes(R.styleable.ListItem);
+
+ mListItemBackgroundColor = a.getColor(R.styleable.ListItem_listItemBackgroundColor,
+ mContext.getColor(R.color.car_card));
mListItemTitleTextAppearance = a.getResourceId(
R.styleable.ListItem_listItemTitleTextAppearance,
R.style.TextAppearance_Car_Body1);
@@ -211,8 +214,6 @@
* Creates a view with background set by {@link BackgroundStyle}.
*/
private ViewGroup createListItemContainer() {
- TypedArray a = mContext.obtainStyledAttributes(mListItemStyle, R.styleable.ListItem);
-
ViewGroup container;
switch (mBackgroundStyle) {
case BackgroundStyle.NONE:
@@ -220,9 +221,7 @@
FrameLayout frameLayout = new FrameLayout(mContext);
frameLayout.setLayoutParams(new RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
- frameLayout.setBackgroundColor(a.getColor(
- R.styleable.ListItem_listItemBackgroundColor,
- mContext.getResources().getColor(R.color.car_card)));
+ frameLayout.setBackgroundColor(mListItemBackgroundColor);
container = frameLayout;
break;
@@ -234,9 +233,7 @@
R.dimen.car_padding_3);
card.setLayoutParams(cardLayoutParams);
card.setRadius(mContext.getResources().getDimensionPixelSize(R.dimen.car_radius_1));
- card.setCardBackgroundColor(a.getColor(
- R.styleable.ListItem_listItemBackgroundColor,
- mContext.getResources().getColor(R.color.car_card)));
+ card.setCardBackgroundColor(mListItemBackgroundColor);
container = card;
break;
@@ -244,8 +241,6 @@
throw new IllegalArgumentException("Unknown background style. "
+ "Expected constants in class ListItemAdapter.BackgroundStyle.");
}
-
- a.recycle();
return container;
}
@@ -293,14 +288,4 @@
return position >= 0 && position < getItemCount()
&& mItemProvider.get(position).shouldHideDivider();
}
-
- /**
- * Returns the style that has been assigned to {@code listItemStyle} in the
- * current theme that is inflating this {@code ListItemAdapter}.
- */
- private static int getListItemStyle(Context context) {
- TypedValue outValue = new TypedValue();
- context.getTheme().resolveAttribute(R.attr.listItemStyle, outValue, true);
- return outValue.resourceId;
- }
}
diff --git a/compat/api/current.txt b/compat/api/current.txt
index faf6744..143e239 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -946,10 +946,10 @@
package androidx.core.hardware.display {
- public abstract class DisplayManagerCompat {
- method public abstract android.view.Display getDisplay(int);
- method public abstract android.view.Display[] getDisplays();
- method public abstract android.view.Display[] getDisplays(java.lang.String);
+ public final class DisplayManagerCompat {
+ method public android.view.Display getDisplay(int);
+ method public android.view.Display[] getDisplays();
+ method public android.view.Display[] getDisplays(java.lang.String);
method public static androidx.core.hardware.display.DisplayManagerCompat getInstance(android.content.Context);
field public static final java.lang.String DISPLAY_CATEGORY_PRESENTATION = "android.hardware.display.category.PRESENTATION";
}
@@ -2489,7 +2489,7 @@
method public static void scrollListBy(android.widget.ListView, int);
}
- public class NestedScrollView extends android.widget.FrameLayout implements androidx.core.view.NestedScrollingChild2 androidx.core.view.NestedScrollingParent androidx.core.view.ScrollingView {
+ public class NestedScrollView extends android.widget.FrameLayout implements androidx.core.view.NestedScrollingChild2 androidx.core.view.NestedScrollingParent2 androidx.core.view.ScrollingView {
ctor public NestedScrollView(android.content.Context);
ctor public NestedScrollView(android.content.Context, android.util.AttributeSet);
ctor public NestedScrollView(android.content.Context, android.util.AttributeSet, int);
@@ -2511,6 +2511,11 @@
method public boolean isFillViewport();
method public boolean isSmoothScrollingEnabled();
method public void onAttachedToWindow();
+ method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+ method public void onNestedScroll(android.view.View, int, int, int, int, int);
+ method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+ method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+ method public void onStopNestedScroll(android.view.View, int);
method public boolean pageScroll(int);
method public void setFillViewport(boolean);
method public void setOnScrollChangeListener(androidx.core.widget.NestedScrollView.OnScrollChangeListener);
diff --git a/compat/src/androidTest/AndroidManifest.xml b/compat/src/androidTest/AndroidManifest.xml
index c0e97c2..2cad9bf 100644
--- a/compat/src/androidTest/AndroidManifest.xml
+++ b/compat/src/androidTest/AndroidManifest.xml
@@ -35,6 +35,8 @@
<activity android:name="androidx.core.widget.TextViewTestActivity"/>
+ <activity android:name="androidx.core.widget.TestContentViewActivity"/>
+
<activity android:name="androidx.core.view.VpaActivity"/>
<activity
diff --git a/compat/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParentTest.java b/compat/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParentTest.java
new file mode 100644
index 0000000..17f68eb
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingParentTest.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright 2018 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 androidx.core.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.test.R;
+import androidx.core.view.NestedScrollingChild2;
+import androidx.core.view.NestedScrollingParent2;
+import androidx.core.view.ViewCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * So far these tests only cover {@code NestedScrollView}'s implementation of
+ * {@link NestedScrollingParent2} and the backwards compatibility of {@code NestedScrollView}'s
+ * implementation of {@link androidx.core.view.NestedScrollingParent} for the methods that
+ * {@link NestedScrollingParent2} overloads.
+ */
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NestedScrollViewNestedScrollingParentTest extends
+ BaseInstrumentationTestCase<TestContentViewActivity> {
+
+ private NestedScrollView mNestedScrollView;
+ private NestedScrollingSpyView mParent;
+ private View mChild;
+
+ public NestedScrollViewNestedScrollingParentTest() {
+ super(TestContentViewActivity.class);
+ }
+
+ @Before
+ public void instantiateMembers() {
+ mNestedScrollView = new NestedScrollView(mActivityTestRule.getActivity());
+ mParent = spy(new NestedScrollingSpyView(mActivityTestRule.getActivity()));
+ mChild = new View(mActivityTestRule.getActivity());
+ }
+
+ @Test
+ public void onStartNestedScroll_scrollAxisIncludesVertical_alwaysReturnsTrue() {
+ int vertical = ViewCompat.SCROLL_AXIS_VERTICAL;
+ int both = ViewCompat.SCROLL_AXIS_VERTICAL | ViewCompat.SCROLL_AXIS_HORIZONTAL;
+
+ onStartNestedScrollV1(vertical, true);
+ onStartNestedScrollV1(both, true);
+
+ onStartNestedScrollV2(vertical, true);
+ onStartNestedScrollV2(both, true);
+ }
+
+ @Test
+ public void onStartNestedScroll_scrollAxisExcludesVertical_alwaysReturnsFalse() {
+ int horizontal = ViewCompat.SCROLL_AXIS_HORIZONTAL;
+ int neither = ViewCompat.SCROLL_AXIS_NONE;
+
+ onStartNestedScrollV1(horizontal, false);
+ onStartNestedScrollV1(neither, false);
+
+ onStartNestedScrollV2(horizontal, false);
+ onStartNestedScrollV2(neither, false);
+ }
+
+ @Test
+ public void onNestedScrollAccepted_callsParentsOnStartNestedScrollWithCorrectParams()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent).onStartNestedScroll(mNestedScrollView, mNestedScrollView,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScrollAccepted_callsParentsOnNestedScrollAcceptedWithCorrectParams()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+
+ mNestedScrollView.onNestedScrollAccepted(
+ mChild,
+ mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL,
+ ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedScrollAccepted(
+ mNestedScrollView,
+ mNestedScrollView,
+ ViewCompat.SCROLL_AXIS_VERTICAL,
+ ViewCompat.TYPE_NON_TOUCH);
+ verify(mParent, times(1)).onNestedScrollAccepted(
+ any(View.class),
+ any(View.class),
+ anyInt(),
+ anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScrollAccepted_withBothOrientations_pOnNestedScrollAcceptedCalledWithVert()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+
+ mNestedScrollView.onNestedScrollAccepted(
+ mChild,
+ mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL | ViewCompat.SCROLL_AXIS_HORIZONTAL,
+ ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedScrollAccepted(
+ any(View.class),
+ any(View.class),
+ eq(ViewCompat.SCROLL_AXIS_VERTICAL),
+ anyInt());
+ verify(mParent, times(1)).onNestedScrollAccepted(
+ any(View.class),
+ any(View.class),
+ anyInt(),
+ anyInt());
+
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScrollAccepted_parentRejects_parentOnNestedScrollAcceptedNotCalled()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(false)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+
+ mNestedScrollView.onNestedScrollAccepted(
+ mChild,
+ mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL,
+ ViewCompat.TYPE_TOUCH);
+
+ verify(mParent, never()).onNestedScrollAccepted(
+ any(View.class),
+ any(View.class),
+ anyInt(),
+ anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScrollAccepted_v1_callsParentWithTypeTouch() throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+
+ mNestedScrollView.onNestedScrollAccepted(
+ mChild,
+ mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL);
+
+ verify(mParent, times(1)).onNestedScrollAccepted(
+ any(View.class),
+ any(View.class),
+ anyInt(),
+ eq(ViewCompat.TYPE_TOUCH));
+ verify(mParent, times(1)).onNestedScrollAccepted(
+ any(View.class),
+ any(View.class),
+ anyInt(),
+ anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onStopNestedScroll_parentOnStopNestedScrollCalledWithCorrectParams()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+
+ mNestedScrollView.onStopNestedScroll(mChild, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onStopNestedScroll(mNestedScrollView,
+ ViewCompat.TYPE_NON_TOUCH);
+ verify(mParent, times(1)).onStopNestedScroll(any(View.class), anyInt());
+ }
+ });
+ }
+
+ // TODO(shepshapard), test with interactions where scroll type changes.
+
+ @Test
+ public void onStopNestedScroll_parentRejects_parentOnStopNestedScrollNotCalled()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(false)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL);
+
+ mNestedScrollView.onStopNestedScroll(mChild, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, never()).onStopNestedScroll(any(View.class), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onStopNestedScroll_calledWithTypeNotYetAccepted_parentOnStopNestedScrollNotCalled()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
+
+ mNestedScrollView.onStopNestedScroll(mChild, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, never()).onStopNestedScroll(any(View.class), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onStopNestedScroll_v1_parentOnStopNestedScrollCalledWithTypeTouch()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL);
+
+ mNestedScrollView.onStopNestedScroll(mChild);
+
+ verify(mParent, times(1)).onStopNestedScroll(any(View.class),
+ eq(ViewCompat.TYPE_TOUCH));
+ verify(mParent, times(1)).onStopNestedScroll(any(View.class), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_nsvScrolls() throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 50, ViewCompat.TYPE_NON_TOUCH);
+
+ assertThat(mNestedScrollView.getScrollY(), is(50));
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_negativeScroll_nsvScrollsNegative() throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mNestedScrollView.scrollTo(0, 50);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, -50, ViewCompat.TYPE_NON_TOUCH);
+
+ assertThat(mNestedScrollView.getScrollY(), is(0));
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_nsvConsumesEntireScroll_correctScrollDistancesPastToParent()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 50, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedScroll(any(View.class), eq(0), eq(50), eq(0),
+ eq(0), anyInt());
+ verify(mParent, times(1)).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_nsvCanOnlyConsumePartOfScroll_correctScrollDistancesPastToParent()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 75, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedScroll(any(View.class), eq(0), eq(50), eq(0),
+ eq(25), anyInt());
+ verify(mParent, times(1)).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_nsvCanOnlyConsumePartOfScrollNeg_correctScrollDistancesPastToParent()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+ mNestedScrollView.scrollTo(0, 50);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, -75, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedScroll(any(View.class), eq(0), eq(-50), eq(0),
+ eq(-25), anyInt());
+ verify(mParent, times(1)).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_nsvIsAtEndOfScroll_correctScrollDistancesPastToParent()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+ mNestedScrollView.scrollTo(0, 50);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 50, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedScroll(any(View.class), eq(0), eq(0), eq(0),
+ eq(50), anyInt());
+ verify(mParent, times(1)).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_parentRejects_parentOnNestedScrollNotCalled() throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(false)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 50, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, never()).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_calledWithTypeNotYetAccepted_parentOnStopNestedScrollNotCalled()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 50, ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, never()).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedScroll_v1_parentOnNestedScrollCalledWithTypeTouch()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild(50, 100);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL);
+
+ mNestedScrollView.onNestedScroll(mChild, 0, 0, 0, 50);
+
+ verify(mParent, times(1)).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), eq(ViewCompat.TYPE_TOUCH));
+ verify(mParent, times(1)).onNestedScroll(any(View.class), anyInt(), anyInt(),
+ anyInt(), anyInt(), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedPreScroll_parentOnNestedPreScrollCalledWithCorrectParams()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+
+ mNestedScrollView.onNestedPreScroll(mChild, 1, 2, new int[]{0, 0},
+ ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, times(1)).onNestedPreScroll(eq(mNestedScrollView), eq(1), eq(2),
+ eq(new int[]{0, 0}), eq(ViewCompat.TYPE_NON_TOUCH));
+ verify(mParent, times(1)).onNestedPreScroll(any(View.class), anyInt(), anyInt(),
+ any(int[].class), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedPreScroll_parentRejects_parentOnNestedPreScrollNotCalled()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(false)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+
+ mNestedScrollView.onNestedPreScroll(mChild, 1, 2, new int[2],
+ ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, never()).onNestedPreScroll(any(View.class), anyInt(), anyInt(),
+ any(int[].class), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedPreScroll_calledWithTypeNotYetAccepted_parentOnStopNestedScrollNotCalled()
+ throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
+
+ mNestedScrollView.onNestedPreScroll(mChild, 1, 2, new int[2],
+ ViewCompat.TYPE_NON_TOUCH);
+
+ verify(mParent, never()).onNestedPreScroll(any(View.class), anyInt(), anyInt(),
+ any(int[].class), anyInt());
+ }
+ });
+ }
+
+ @Test
+ public void onNestedPreScroll_v1_parentOnNestedPreScrollCalledWithTypeTouch() throws Throwable {
+ setupNestedScrollViewWithParentAndChild();
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doReturn(true)
+ .when(mParent)
+ .onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt());
+ mNestedScrollView.onNestedScrollAccepted(mChild, mChild,
+ ViewCompat.SCROLL_AXIS_VERTICAL);
+ int[] consumed = new int[2];
+
+ mNestedScrollView.onNestedPreScroll(mChild, 1, 2, consumed);
+
+ verify(mParent, times(1)).onNestedPreScroll(any(View.class), anyInt(), anyInt(),
+ any(int[].class), eq(ViewCompat.TYPE_TOUCH));
+ verify(mParent, times(1)).onNestedPreScroll(any(View.class), anyInt(), anyInt(),
+ any(int[].class), anyInt());
+ }
+ });
+ }
+
+ private void onStartNestedScrollV1(int iScrollAxis, boolean oRetValue) {
+ boolean retVal = mNestedScrollView.onStartNestedScroll(mChild, mChild, iScrollAxis);
+ assertThat(retVal, is(oRetValue));
+ }
+
+ private void onStartNestedScrollV2(int iScrollAxis, boolean oRetValue) {
+ boolean retVal = mNestedScrollView.onStartNestedScroll(mChild, mChild, iScrollAxis,
+ ViewCompat.TYPE_TOUCH);
+ assertThat(retVal, is(oRetValue));
+ }
+
+ private void setupNestedScrollViewWithParentAndChild() throws Throwable {
+ setupNestedScrollViewWithParentAndChild(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+
+ private void setupNestedScrollViewWithParentAndChild(int nestedScrollViewHeight,
+ int childHeight) throws Throwable {
+ final TestContentView testContentView =
+ mActivityTestRule.getActivity().findViewById(R.id.testContentView);
+
+ mNestedScrollView.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, nestedScrollViewHeight));
+ mNestedScrollView.setMinimumHeight(nestedScrollViewHeight);
+
+ mChild.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, childHeight));
+ mChild.setMinimumHeight(childHeight);
+
+ testContentView.expectLayouts(1);
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mNestedScrollView.addView(mChild);
+ mParent.addView(mNestedScrollView);
+ testContentView.addView(mParent);
+ }
+ });
+ testContentView.awaitLayouts(2);
+ }
+
+ public class NestedScrollingSpyView extends FrameLayout implements NestedScrollingChild2,
+ NestedScrollingParent2 {
+
+ public NestedScrollingSpyView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
+ int type) {
+ return false;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
+ int type) {
+
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull View target, int type) {
+
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed, int type) {
+
+ }
+
+ @Override
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
+ int type) {
+
+ }
+
+ @Override
+ public boolean startNestedScroll(int axes, int type) {
+ return false;
+ }
+
+ @Override
+ public void stopNestedScroll(int type) {
+
+ }
+
+ @Override
+ public boolean hasNestedScrollingParent(int type) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+ int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+ return false;
+ }
+
+ @Override
+ public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
+ @Nullable int[] offsetInWindow, int type) {
+ return false;
+ }
+ }
+
+}
diff --git a/compat/src/androidTest/java/androidx/core/widget/TestContentView.java b/compat/src/androidTest/java/androidx/core/widget/TestContentView.java
new file mode 100644
index 0000000..510041a
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/widget/TestContentView.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 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 androidx.core.widget;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TestContentView extends FrameLayout {
+
+ private CountDownLatch mLayoutCountDownLatch;
+
+ public TestContentView(@NonNull Context context) {
+ super(context);
+ }
+
+ public TestContentView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TestContentView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @SuppressWarnings("unused")
+ public TestContentView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void expectLayouts(int count) {
+ mLayoutCountDownLatch = new CountDownLatch(count);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ public void awaitLayouts(int seconds) throws InterruptedException {
+ assertThat(mLayoutCountDownLatch.await(seconds, TimeUnit.SECONDS), equalTo(true));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mLayoutCountDownLatch != null) {
+ mLayoutCountDownLatch.countDown();
+ }
+ }
+}
diff --git a/compat/src/androidTest/java/androidx/core/widget/TestContentViewActivity.java b/compat/src/androidTest/java/androidx/core/widget/TestContentViewActivity.java
new file mode 100644
index 0000000..5ecb46d
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/widget/TestContentViewActivity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018 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 androidx.core.widget;
+
+import android.support.v4.BaseTestActivity;
+
+import androidx.core.test.R;
+
+public class TestContentViewActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.test_content_view;
+ }
+}
diff --git a/compat/src/androidTest/res/layout/test_content_view.xml b/compat/src/androidTest/res/layout/test_content_view.xml
new file mode 100644
index 0000000..6b179ad
--- /dev/null
+++ b/compat/src/androidTest/res/layout/test_content_view.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 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.
+ -->
+
+<androidx.core.widget.TestContentView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/testContentView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/compat/src/main/java/androidx/core/app/ActivityOptionsCompat.java b/compat/src/main/java/androidx/core/app/ActivityOptionsCompat.java
index 3f2c8ab..49f6c4a 100644
--- a/compat/src/main/java/androidx/core/app/ActivityOptionsCompat.java
+++ b/compat/src/main/java/androidx/core/app/ActivityOptionsCompat.java
@@ -66,7 +66,8 @@
public static ActivityOptionsCompat makeCustomAnimation(@NonNull Context context,
int enterResId, int exitResId) {
if (Build.VERSION.SDK_INT >= 16) {
- return createImpl(ActivityOptions.makeCustomAnimation(context, enterResId, exitResId));
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeCustomAnimation(context,
+ enterResId, exitResId));
}
return new ActivityOptionsCompat();
}
@@ -95,7 +96,7 @@
public static ActivityOptionsCompat makeScaleUpAnimation(@NonNull View source,
int startX, int startY, int startWidth, int startHeight) {
if (Build.VERSION.SDK_INT >= 16) {
- return createImpl(ActivityOptions.makeScaleUpAnimation(
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeScaleUpAnimation(
source, startX, startY, startWidth, startHeight));
}
return new ActivityOptionsCompat();
@@ -119,7 +120,7 @@
public static ActivityOptionsCompat makeClipRevealAnimation(@NonNull View source,
int startX, int startY, int width, int height) {
if (Build.VERSION.SDK_INT >= 23) {
- return createImpl(ActivityOptions.makeClipRevealAnimation(
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeClipRevealAnimation(
source, startX, startY, width, height));
}
return new ActivityOptionsCompat();
@@ -148,7 +149,7 @@
public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(@NonNull View source,
@NonNull Bitmap thumbnail, int startX, int startY) {
if (Build.VERSION.SDK_INT >= 16) {
- return createImpl(ActivityOptions.makeThumbnailScaleUpAnimation(
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeThumbnailScaleUpAnimation(
source, thumbnail, startX, startY));
}
return new ActivityOptionsCompat();
@@ -176,7 +177,7 @@
public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity,
@NonNull View sharedElement, @NonNull String sharedElementName) {
if (Build.VERSION.SDK_INT >= 21) {
- return createImpl(ActivityOptions.makeSceneTransitionAnimation(
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeSceneTransitionAnimation(
activity, sharedElement, sharedElementName));
}
return new ActivityOptionsCompat();
@@ -212,7 +213,8 @@
sharedElements[i].first, sharedElements[i].second);
}
}
- return createImpl(ActivityOptions.makeSceneTransitionAnimation(activity, pairs));
+ return new ActivityOptionsCompatImpl(
+ ActivityOptions.makeSceneTransitionAnimation(activity, pairs));
}
return new ActivityOptionsCompat();
}
@@ -230,7 +232,7 @@
@NonNull
public static ActivityOptionsCompat makeTaskLaunchBehind() {
if (Build.VERSION.SDK_INT >= 21) {
- return createImpl(ActivityOptions.makeTaskLaunchBehind());
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeTaskLaunchBehind());
}
return new ActivityOptionsCompat();
}
@@ -242,27 +244,16 @@
@NonNull
public static ActivityOptionsCompat makeBasic() {
if (Build.VERSION.SDK_INT >= 23) {
- return createImpl(ActivityOptions.makeBasic());
+ return new ActivityOptionsCompatImpl(ActivityOptions.makeBasic());
}
return new ActivityOptionsCompat();
}
@RequiresApi(16)
- private static ActivityOptionsCompat createImpl(ActivityOptions options) {
- if (Build.VERSION.SDK_INT >= 24) {
- return new ActivityOptionsCompatApi24Impl(options);
- } else if (Build.VERSION.SDK_INT >= 23) {
- return new ActivityOptionsCompatApi23Impl(options);
- } else {
- return new ActivityOptionsCompatApi16Impl(options);
- }
- }
+ private static class ActivityOptionsCompatImpl extends ActivityOptionsCompat {
+ private final ActivityOptions mActivityOptions;
- @RequiresApi(16)
- private static class ActivityOptionsCompatApi16Impl extends ActivityOptionsCompat {
- protected final ActivityOptions mActivityOptions;
-
- ActivityOptionsCompatApi16Impl(ActivityOptions activityOptions) {
+ ActivityOptionsCompatImpl(ActivityOptions activityOptions) {
mActivityOptions = activityOptions;
}
@@ -273,35 +264,21 @@
@Override
public void update(ActivityOptionsCompat otherOptions) {
- if (otherOptions instanceof ActivityOptionsCompatApi16Impl) {
- ActivityOptionsCompatApi16Impl otherImpl =
- (ActivityOptionsCompatApi16Impl) otherOptions;
+ if (otherOptions instanceof ActivityOptionsCompatImpl) {
+ ActivityOptionsCompatImpl otherImpl =
+ (ActivityOptionsCompatImpl) otherOptions;
mActivityOptions.update(otherImpl.mActivityOptions);
}
}
- }
-
- @RequiresApi(23)
- private static class ActivityOptionsCompatApi23Impl extends ActivityOptionsCompatApi16Impl {
- ActivityOptionsCompatApi23Impl(ActivityOptions activityOptions) {
- super(activityOptions);
- }
@Override
public void requestUsageTimeReport(PendingIntent receiver) {
mActivityOptions.requestUsageTimeReport(receiver);
}
- }
-
- @RequiresApi(24)
- private static class ActivityOptionsCompatApi24Impl extends ActivityOptionsCompatApi23Impl {
- ActivityOptionsCompatApi24Impl(ActivityOptions activityOptions) {
- super(activityOptions);
- }
@Override
public ActivityOptionsCompat setLaunchBounds(@Nullable Rect screenSpacePixelRect) {
- return new ActivityOptionsCompatApi24Impl(
+ return new ActivityOptionsCompatImpl(
mActivityOptions.setLaunchBounds(screenSpacePixelRect));
}
diff --git a/compat/src/main/java/androidx/core/app/NotificationCompatBuilder.java b/compat/src/main/java/androidx/core/app/NotificationCompatBuilder.java
index 6be24ff..6ec420a 100644
--- a/compat/src/main/java/androidx/core/app/NotificationCompatBuilder.java
+++ b/compat/src/main/java/androidx/core/app/NotificationCompatBuilder.java
@@ -153,25 +153,28 @@
}
mHeadsUpContentView = b.mHeadsUpContentView;
- // Invisible actions should be stored in the extender so we need to check if one exists
- // already.
- Bundle carExtenderBundle =
- b.getExtras().getBundle(NotificationCompat.CarExtender.EXTRA_CAR_EXTENDER);
- if (carExtenderBundle == null) {
- carExtenderBundle = new Bundle();
+ if (b.mInvisibleActions.size() > 0) {
+ // Invisible actions should be stored in the extender so we need to check if one
+ // exists already.
+ Bundle carExtenderBundle =
+ b.getExtras().getBundle(NotificationCompat.CarExtender.EXTRA_CAR_EXTENDER);
+ if (carExtenderBundle == null) {
+ carExtenderBundle = new Bundle();
+ }
+ Bundle listBundle = new Bundle();
+ for (int i = 0; i < b.mInvisibleActions.size(); i++) {
+ listBundle.putBundle(
+ Integer.toString(i),
+ NotificationCompatJellybean.getBundleForAction(
+ b.mInvisibleActions.get(i)));
+ }
+ carExtenderBundle.putBundle(
+ NotificationCompat.CarExtender.EXTRA_INVISIBLE_ACTIONS, listBundle);
+ b.getExtras().putBundle(
+ NotificationCompat.CarExtender.EXTRA_CAR_EXTENDER, carExtenderBundle);
+ mExtras.putBundle(
+ NotificationCompat.CarExtender.EXTRA_CAR_EXTENDER, carExtenderBundle);
}
- Bundle listBundle = new Bundle();
- for (int i = 0; i < b.mInvisibleActions.size(); i++) {
- listBundle.putBundle(
- Integer.toString(i),
- NotificationCompatJellybean.getBundleForAction(b.mInvisibleActions.get(i)));
- }
- carExtenderBundle.putBundle(
- NotificationCompat.CarExtender.EXTRA_INVISIBLE_ACTIONS, listBundle);
- b.getExtras().putBundle(
- NotificationCompat.CarExtender.EXTRA_CAR_EXTENDER, carExtenderBundle);
- mExtras.putBundle(
- NotificationCompat.CarExtender.EXTRA_CAR_EXTENDER, carExtenderBundle);
}
if (Build.VERSION.SDK_INT >= 24) {
mBuilder.setExtras(b.mExtras)
diff --git a/compat/src/main/java/androidx/core/app/SupportActivity.java b/compat/src/main/java/androidx/core/app/SupportActivity.java
index 5ec33ec..1e02c0a 100644
--- a/compat/src/main/java/androidx/core/app/SupportActivity.java
+++ b/compat/src/main/java/androidx/core/app/SupportActivity.java
@@ -19,16 +19,16 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.app.Activity;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.LifecycleRegistry;
-import android.arch.lifecycle.ReportFragment;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.collection.SimpleArrayMap;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.ReportFragment;
/**
* Base class for composing together compatibility functionality
diff --git a/compat/src/main/java/androidx/core/app/TaskStackBuilder.java b/compat/src/main/java/androidx/core/app/TaskStackBuilder.java
index c3deff6..a059eac 100644
--- a/compat/src/main/java/androidx/core/app/TaskStackBuilder.java
+++ b/compat/src/main/java/androidx/core/app/TaskStackBuilder.java
@@ -28,7 +28,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
@@ -77,36 +76,6 @@
Intent getSupportParentActivityIntent();
}
- static class TaskStackBuilderBaseImpl {
- public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode,
- int flags, Bundle options) {
- intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- return PendingIntent.getActivities(context, requestCode, intents, flags);
- }
- }
-
- @RequiresApi(16)
- static class TaskStackBuilderApi16Impl extends TaskStackBuilderBaseImpl {
- @Override
- public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode,
- int flags, Bundle options) {
- intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- return PendingIntent.getActivities(context, requestCode, intents, flags, options);
- }
- }
-
- private static final TaskStackBuilderBaseImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 16) {
- IMPL = new TaskStackBuilderApi16Impl();
- } else {
- IMPL = new TaskStackBuilderBaseImpl();
- }
- }
-
private final ArrayList<Intent> mIntents = new ArrayList<Intent>();
private final Context mSourceContext;
@@ -367,8 +336,13 @@
Intent[] intents = mIntents.toArray(new Intent[mIntents.size()]);
intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
- // Appropriate flags will be added by the call below.
- return IMPL.getPendingIntent(mSourceContext, intents, requestCode, flags, options);
+
+ if (Build.VERSION.SDK_INT >= 16) {
+ return PendingIntent.getActivities(mSourceContext, requestCode, intents, flags,
+ options);
+ } else {
+ return PendingIntent.getActivities(mSourceContext, requestCode, intents, flags);
+ }
}
/**
diff --git a/compat/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java b/compat/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
index 0276654..ff927a6 100644
--- a/compat/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
+++ b/compat/src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java
@@ -24,14 +24,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import java.util.WeakHashMap;
/**
* Helper for accessing features in {@link android.hardware.display.DisplayManager}.
*/
-public abstract class DisplayManagerCompat {
+public final class DisplayManagerCompat {
private static final WeakHashMap<Context, DisplayManagerCompat> sInstances =
new WeakHashMap<Context, DisplayManagerCompat>();
@@ -49,7 +48,10 @@
public static final String DISPLAY_CATEGORY_PRESENTATION =
"android.hardware.display.category.PRESENTATION";
- DisplayManagerCompat() {
+ private final Context mContext;
+
+ private DisplayManagerCompat(Context context) {
+ mContext = context;
}
/**
@@ -60,11 +62,7 @@
synchronized (sInstances) {
DisplayManagerCompat instance = sInstances.get(context);
if (instance == null) {
- if (Build.VERSION.SDK_INT >= 17) {
- instance = new DisplayManagerCompatApi17Impl(context);
- } else {
- instance = new DisplayManagerCompatApi14Impl(context);
- }
+ instance = new DisplayManagerCompat(context);
sInstances.put(context, instance);
}
return instance;
@@ -81,7 +79,19 @@
* @return The display object, or null if there is no valid display with the given id.
*/
@Nullable
- public abstract Display getDisplay(int displayId);
+ public Display getDisplay(int displayId) {
+ if (Build.VERSION.SDK_INT >= 17) {
+ return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE))
+ .getDisplay(displayId);
+ }
+
+ Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ if (display.getDisplayId() == displayId) {
+ return display;
+ }
+ return null;
+ }
/**
* Gets all currently valid logical displays.
@@ -89,7 +99,16 @@
* @return An array containing all displays.
*/
@NonNull
- public abstract Display[] getDisplays();
+ public Display[] getDisplays() {
+ if (Build.VERSION.SDK_INT >= 17) {
+ return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE))
+ .getDisplays();
+ }
+
+ Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ return new Display[] { display };
+ }
/**
* Gets all currently valid logical displays of the specified category.
@@ -108,56 +127,17 @@
* @see #DISPLAY_CATEGORY_PRESENTATION
*/
@NonNull
- public abstract Display[] getDisplays(String category);
-
- private static class DisplayManagerCompatApi14Impl extends DisplayManagerCompat {
- private final WindowManager mWindowManager;
-
- DisplayManagerCompatApi14Impl(Context context) {
- mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ public Display[] getDisplays(@Nullable String category) {
+ if (Build.VERSION.SDK_INT >= 17) {
+ return ((DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE))
+ .getDisplays(category);
+ }
+ if (category == null) {
+ return new Display[0];
}
- @Override
- public Display getDisplay(int displayId) {
- Display display = mWindowManager.getDefaultDisplay();
- if (display.getDisplayId() == displayId) {
- return display;
- }
- return null;
- }
-
- @Override
- public Display[] getDisplays() {
- return new Display[] { mWindowManager.getDefaultDisplay() };
- }
-
- @Override
- public Display[] getDisplays(String category) {
- return category == null ? getDisplays() : new Display[0];
- }
- }
-
- @RequiresApi(17)
- private static class DisplayManagerCompatApi17Impl extends DisplayManagerCompat {
- private final DisplayManager mDisplayManager;
-
- DisplayManagerCompatApi17Impl(Context context) {
- mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
- }
-
- @Override
- public Display getDisplay(int displayId) {
- return mDisplayManager.getDisplay(displayId);
- }
-
- @Override
- public Display[] getDisplays() {
- return mDisplayManager.getDisplays();
- }
-
- @Override
- public Display[] getDisplays(String category) {
- return mDisplayManager.getDisplays(category);
- }
+ Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay();
+ return new Display[] { display };
}
}
diff --git a/compat/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java b/compat/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
index 00d5ac6..0f36f42 100644
--- a/compat/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
+++ b/compat/src/main/java/androidx/core/view/AccessibilityDelegateCompat.java
@@ -25,7 +25,6 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
-import androidx.annotation.RequiresApi;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
@@ -46,160 +45,75 @@
*/
public class AccessibilityDelegateCompat {
- static class AccessibilityDelegateBaseImpl {
- public AccessibilityDelegate newAccessibilityDelegateBridge(
- final AccessibilityDelegateCompat compat) {
- return new AccessibilityDelegate() {
- @Override
- public boolean dispatchPopulateAccessibilityEvent(View host,
- AccessibilityEvent event) {
- return compat.dispatchPopulateAccessibilityEvent(host, event);
- }
+ private static final class AccessibilityDelegateAdapter extends AccessibilityDelegate {
+ private final AccessibilityDelegateCompat mCompat;
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- compat.onInitializeAccessibilityEvent(host, event);
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(
- View host, AccessibilityNodeInfo info) {
- compat.onInitializeAccessibilityNodeInfo(host,
- AccessibilityNodeInfoCompat.wrap(info));
- }
-
- @Override
- public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
- compat.onPopulateAccessibilityEvent(host, event);
- }
-
- @Override
- public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
- AccessibilityEvent event) {
- return compat.onRequestSendAccessibilityEvent(host, child, event);
- }
-
- @Override
- public void sendAccessibilityEvent(View host, int eventType) {
- compat.sendAccessibilityEvent(host, eventType);
- }
-
- @Override
- public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
- compat.sendAccessibilityEventUnchecked(host, event);
- }
- };
- }
-
- public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(
- AccessibilityDelegate delegate, View host) {
- // Do nothing. Added in API 16.
- return null;
- }
-
- public boolean performAccessibilityAction(AccessibilityDelegate delegate, View host,
- int action, Bundle args) {
- // Do nothing. Added in API 16.
- return false;
- }
- }
-
- @RequiresApi(16)
- static class AccessibilityDelegateApi16Impl extends AccessibilityDelegateBaseImpl {
- @Override
- public AccessibilityDelegate newAccessibilityDelegateBridge(
- final AccessibilityDelegateCompat compat) {
- return new AccessibilityDelegate() {
- @Override
- public boolean dispatchPopulateAccessibilityEvent(View host,
- AccessibilityEvent event) {
- return compat.dispatchPopulateAccessibilityEvent(host, event);
- }
-
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- compat.onInitializeAccessibilityEvent(host, event);
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(
- View host, AccessibilityNodeInfo info) {
- compat.onInitializeAccessibilityNodeInfo(host,
- AccessibilityNodeInfoCompat.wrap(info));
- }
-
- @Override
- public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
- compat.onPopulateAccessibilityEvent(host, event);
- }
-
- @Override
- public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
- AccessibilityEvent event) {
- return compat.onRequestSendAccessibilityEvent(host, child, event);
- }
-
- @Override
- public void sendAccessibilityEvent(View host, int eventType) {
- compat.sendAccessibilityEvent(host, eventType);
- }
-
- @Override
- public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
- compat.sendAccessibilityEventUnchecked(host, event);
- }
-
- @Override
- public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
- AccessibilityNodeProviderCompat provider =
- compat.getAccessibilityNodeProvider(host);
- return (provider != null)
- ? (AccessibilityNodeProvider) provider.getProvider() : null;
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- return compat.performAccessibilityAction(host, action, args);
- }
- };
+ AccessibilityDelegateAdapter(AccessibilityDelegateCompat compat) {
+ mCompat = compat;
}
@Override
- public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(
- AccessibilityDelegate delegate, View host) {
- Object provider = delegate.getAccessibilityNodeProvider(host);
- if (provider != null) {
- return new AccessibilityNodeProviderCompat(provider);
- }
- return null;
+ public boolean dispatchPopulateAccessibilityEvent(View host,
+ AccessibilityEvent event) {
+ return mCompat.dispatchPopulateAccessibilityEvent(host, event);
}
@Override
- public boolean performAccessibilityAction(AccessibilityDelegate delegate, View host,
- int action, Bundle args) {
- return delegate.performAccessibilityAction(host, action, args);
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ mCompat.onInitializeAccessibilityEvent(host, event);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(
+ View host, AccessibilityNodeInfo info) {
+ mCompat.onInitializeAccessibilityNodeInfo(host,
+ AccessibilityNodeInfoCompat.wrap(info));
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+ mCompat.onPopulateAccessibilityEvent(host, event);
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
+ AccessibilityEvent event) {
+ return mCompat.onRequestSendAccessibilityEvent(host, child, event);
+ }
+
+ @Override
+ public void sendAccessibilityEvent(View host, int eventType) {
+ mCompat.sendAccessibilityEvent(host, eventType);
+ }
+
+ @Override
+ public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
+ mCompat.sendAccessibilityEventUnchecked(host, event);
+ }
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ AccessibilityNodeProviderCompat provider =
+ mCompat.getAccessibilityNodeProvider(host);
+ return (provider != null)
+ ? (AccessibilityNodeProvider) provider.getProvider() : null;
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ return mCompat.performAccessibilityAction(host, action, args);
}
}
- private static final AccessibilityDelegateBaseImpl IMPL;
- private static final AccessibilityDelegate DEFAULT_DELEGATE;
+ private static final AccessibilityDelegate DEFAULT_DELEGATE = new AccessibilityDelegate();
- static {
- if (Build.VERSION.SDK_INT >= 16) { // JellyBean
- IMPL = new AccessibilityDelegateApi16Impl();
- } else {
- IMPL = new AccessibilityDelegateBaseImpl();
- }
- DEFAULT_DELEGATE = new AccessibilityDelegate();
- }
-
- final AccessibilityDelegate mBridge;
+ private final AccessibilityDelegate mBridge;
/**
* Creates a new instance.
*/
public AccessibilityDelegateCompat() {
- mBridge = IMPL.newAccessibilityDelegateBridge(this);
+ mBridge = new AccessibilityDelegateAdapter(this);
}
/**
@@ -369,7 +283,13 @@
* @see AccessibilityNodeProviderCompat
*/
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
- return IMPL.getAccessibilityNodeProvider(DEFAULT_DELEGATE, host);
+ if (Build.VERSION.SDK_INT >= 16) {
+ Object provider = DEFAULT_DELEGATE.getAccessibilityNodeProvider(host);
+ if (provider != null) {
+ return new AccessibilityNodeProviderCompat(provider);
+ }
+ }
+ return null;
}
/**
@@ -389,6 +309,9 @@
* View#performAccessibilityAction(int, Bundle)
*/
public boolean performAccessibilityAction(View host, int action, Bundle args) {
- return IMPL.performAccessibilityAction(DEFAULT_DELEGATE, host, action, args);
+ if (Build.VERSION.SDK_INT >= 16) {
+ return DEFAULT_DELEGATE.performAccessibilityAction(host, action, args);
+ }
+ return false;
}
}
diff --git a/compat/src/main/java/androidx/core/view/LayoutInflaterCompat.java b/compat/src/main/java/androidx/core/view/LayoutInflaterCompat.java
index 889f9a3..437d56a 100644
--- a/compat/src/main/java/androidx/core/view/LayoutInflaterCompat.java
+++ b/compat/src/main/java/androidx/core/view/LayoutInflaterCompat.java
@@ -24,7 +24,6 @@
import android.view.View;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import java.lang.reflect.Field;
@@ -68,7 +67,7 @@
* that already had a Factory2 registered. We work around that bug here. If we can't we
* log an error.
*/
- static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
+ private static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
if (!sCheckedField) {
try {
sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
@@ -90,61 +89,6 @@
}
}
- static class LayoutInflaterCompatBaseImpl {
- @SuppressWarnings("deprecation")
- public void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
- final LayoutInflater.Factory2 factory2 = factory != null
- ? new Factory2Wrapper(factory) : null;
- setFactory2(inflater, factory2);
- }
-
- public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
- inflater.setFactory2(factory);
-
- final LayoutInflater.Factory f = inflater.getFactory();
- if (f instanceof LayoutInflater.Factory2) {
- // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
- // We will now try and force set the merged factory to mFactory2
- forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
- } else {
- // Else, we will force set the original wrapped Factory2
- forceSetFactory2(inflater, factory);
- }
- }
-
- @SuppressWarnings("deprecation")
- public LayoutInflaterFactory getFactory(LayoutInflater inflater) {
- LayoutInflater.Factory factory = inflater.getFactory();
- if (factory instanceof Factory2Wrapper) {
- return ((Factory2Wrapper) factory).mDelegateFactory;
- }
- return null;
- }
- }
-
- @RequiresApi(21)
- static class LayoutInflaterCompatApi21Impl extends LayoutInflaterCompatBaseImpl {
- @SuppressWarnings("deprecation")
- @Override
- public void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
- inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
- }
-
- @Override
- public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
- inflater.setFactory2(factory);
- }
- }
-
- static final LayoutInflaterCompatBaseImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new LayoutInflaterCompatApi21Impl();
- } else {
- IMPL = new LayoutInflaterCompatBaseImpl();
- }
- }
-
/*
* Hide the constructor.
*/
@@ -164,7 +108,23 @@
@Deprecated
public static void setFactory(
@NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
- IMPL.setFactory(inflater, factory);
+ if (Build.VERSION.SDK_INT >= 21) {
+ inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
+ } else {
+ final LayoutInflater.Factory2 factory2 = factory != null
+ ? new Factory2Wrapper(factory) : null;
+ inflater.setFactory2(factory2);
+
+ final LayoutInflater.Factory f = inflater.getFactory();
+ if (f instanceof LayoutInflater.Factory2) {
+ // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
+ // We will now try and force set the merged factory to mFactory2
+ forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
+ } else {
+ // Else, we will force set the original wrapped Factory2
+ forceSetFactory2(inflater, factory2);
+ }
+ }
}
/**
@@ -176,7 +136,19 @@
*/
public static void setFactory2(
@NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
- IMPL.setFactory2(inflater, factory);
+ inflater.setFactory2(factory);
+
+ if (Build.VERSION.SDK_INT < 21) {
+ final LayoutInflater.Factory f = inflater.getFactory();
+ if (f instanceof LayoutInflater.Factory2) {
+ // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
+ // We will now try and force set the merged factory to mFactory2
+ forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
+ } else {
+ // Else, we will force set the original wrapped Factory2
+ forceSetFactory2(inflater, factory);
+ }
+ }
}
/**
@@ -194,6 +166,10 @@
*/
@Deprecated
public static LayoutInflaterFactory getFactory(LayoutInflater inflater) {
- return IMPL.getFactory(inflater);
+ LayoutInflater.Factory factory = inflater.getFactory();
+ if (factory instanceof Factory2Wrapper) {
+ return ((Factory2Wrapper) factory).mDelegateFactory;
+ }
+ return null;
}
}
diff --git a/compat/src/main/java/androidx/core/view/MenuItemCompat.java b/compat/src/main/java/androidx/core/view/MenuItemCompat.java
index d367052..c6b77e5 100644
--- a/compat/src/main/java/androidx/core/view/MenuItemCompat.java
+++ b/compat/src/main/java/androidx/core/view/MenuItemCompat.java
@@ -26,7 +26,6 @@
import android.view.MenuItem;
import android.view.View;
-import androidx.annotation.RequiresApi;
import androidx.core.internal.view.SupportMenuItem;
/**
@@ -88,26 +87,6 @@
public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8;
/**
- * Interface for the full API.
- */
- interface MenuVersionImpl {
- void setContentDescription(MenuItem item, CharSequence contentDescription);
- CharSequence getContentDescription(MenuItem item);
- void setTooltipText(MenuItem item, CharSequence tooltipText);
- CharSequence getTooltipText(MenuItem item);
- void setShortcut(MenuItem item, char numericChar, char alphaChar, int numericModifiers,
- int alphaModifiers);
- void setAlphabeticShortcut(MenuItem item, char alphaChar, int alphaModifiers);
- int getAlphabeticModifiers(MenuItem item);
- void setNumericShortcut(MenuItem item, char numericChar, int numericModifiers);
- int getNumericModifiers(MenuItem item);
- void setIconTintList(MenuItem item, ColorStateList tint);
- ColorStateList getIconTintList(MenuItem item);
- void setIconTintMode(MenuItem item, PorterDuff.Mode tintMode);
- PorterDuff.Mode getIconTintMode(MenuItem item);
- }
-
- /**
* Interface definition for a callback to be invoked when a menu item marked with {@link
* #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} is expanded or collapsed.
*
@@ -139,148 +118,6 @@
boolean onMenuItemActionCollapse(MenuItem item);
}
- static class MenuItemCompatBaseImpl implements MenuVersionImpl {
- @Override
- public void setContentDescription(MenuItem item, CharSequence contentDescription) {
- }
-
- @Override
- public CharSequence getContentDescription(MenuItem item) {
- return null;
- }
-
- @Override
- public void setTooltipText(MenuItem item, CharSequence tooltipText) {
- }
-
- @Override
- public CharSequence getTooltipText(MenuItem item) {
- return null;
- }
-
- @Override
- public void setShortcut(MenuItem item, char numericChar, char alphaChar,
- int numericModifiers, int alphaModifiers) {
- }
-
- @Override
- public void setAlphabeticShortcut(MenuItem item, char alphaChar, int alphaModifiers) {
- }
-
- @Override
- public int getAlphabeticModifiers(MenuItem item) {
- return 0;
- }
-
- @Override
- public void setNumericShortcut(MenuItem item, char numericChar, int numericModifiers) {
- }
-
- @Override
- public int getNumericModifiers(MenuItem item) {
- return 0;
- }
-
- @Override
- public void setIconTintList(MenuItem item, ColorStateList tint) {
- }
-
- @Override
- public ColorStateList getIconTintList(MenuItem item) {
- return null;
- }
-
- @Override
- public void setIconTintMode(MenuItem item, PorterDuff.Mode tintMode) {
- }
-
- @Override
- public PorterDuff.Mode getIconTintMode(MenuItem item) {
- return null;
- }
- }
-
- @RequiresApi(26)
- static class MenuItemCompatApi26Impl extends MenuItemCompatBaseImpl {
- @Override
- public void setContentDescription(MenuItem item, CharSequence contentDescription) {
- item.setContentDescription(contentDescription);
- }
-
- @Override
- public CharSequence getContentDescription(MenuItem item) {
- return item.getContentDescription();
- }
-
- @Override
- public void setTooltipText(MenuItem item, CharSequence tooltipText) {
- item.setTooltipText(tooltipText);
- }
-
- @Override
- public CharSequence getTooltipText(MenuItem item) {
- return item.getTooltipText();
- }
-
- @Override
- public void setShortcut(MenuItem item, char numericChar, char alphaChar,
- int numericModifiers, int alphaModifiers) {
- item.setShortcut(numericChar, alphaChar, numericModifiers, alphaModifiers);
- }
-
- @Override
- public void setAlphabeticShortcut(MenuItem item, char alphaChar, int alphaModifiers) {
- item.setAlphabeticShortcut(alphaChar, alphaModifiers);
- }
-
- @Override
- public int getAlphabeticModifiers(MenuItem item) {
- return item.getAlphabeticModifiers();
- }
-
- @Override
- public void setNumericShortcut(MenuItem item, char numericChar, int numericModifiers) {
- item.setNumericShortcut(numericChar, numericModifiers);
- }
-
- @Override
- public int getNumericModifiers(MenuItem item) {
- return item.getNumericModifiers();
- }
-
- @Override
- public void setIconTintList(MenuItem item, ColorStateList tint) {
- item.setIconTintList(tint);
- }
-
- @Override
- public ColorStateList getIconTintList(MenuItem item) {
- return item.getIconTintList();
- }
-
- @Override
- public void setIconTintMode(MenuItem item, PorterDuff.Mode tintMode) {
- item.setIconTintMode(tintMode);
- }
-
- @Override
- public PorterDuff.Mode getIconTintMode(MenuItem item) {
- return item.getIconTintMode();
- }
- }
-
- /**
- * Select the correct implementation to use for the current platform.
- */
- static final MenuVersionImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 26) {
- IMPL = new MenuItemCompatApi26Impl();
- } else {
- IMPL = new MenuItemCompatBaseImpl();
- }
- }
-
// -------------------------------------------------------------------
/**
@@ -484,8 +321,8 @@
public static void setContentDescription(MenuItem item, CharSequence contentDescription) {
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setContentDescription(contentDescription);
- } else {
- IMPL.setContentDescription(item, contentDescription);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setContentDescription(contentDescription);
}
}
@@ -498,7 +335,10 @@
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getContentDescription();
}
- return IMPL.getContentDescription(item);
+ if (Build.VERSION.SDK_INT >= 26) {
+ return item.getContentDescription();
+ }
+ return null;
}
/**
@@ -510,8 +350,8 @@
public static void setTooltipText(MenuItem item, CharSequence tooltipText) {
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setTooltipText(tooltipText);
- } else {
- IMPL.setTooltipText(item, tooltipText);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setTooltipText(tooltipText);
}
}
@@ -524,7 +364,10 @@
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getTooltipText();
}
- return IMPL.getTooltipText(item);
+ if (Build.VERSION.SDK_INT >= 26) {
+ return item.getTooltipText();
+ }
+ return null;
}
/**
@@ -554,8 +397,8 @@
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setShortcut(numericChar, alphaChar, numericModifiers,
alphaModifiers);
- } else {
- IMPL.setShortcut(item, numericChar, alphaChar, numericModifiers, alphaModifiers);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setShortcut(numericChar, alphaChar, numericModifiers, alphaModifiers);
}
}
@@ -574,8 +417,8 @@
public static void setNumericShortcut(MenuItem item, char numericChar, int numericModifiers) {
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setNumericShortcut(numericChar, numericModifiers);
- } else {
- IMPL.setNumericShortcut(item, numericChar, numericModifiers);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setNumericShortcut(numericChar, numericModifiers);
}
}
@@ -593,7 +436,10 @@
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getNumericModifiers();
}
- return IMPL.getNumericModifiers(item);
+ if (Build.VERSION.SDK_INT >= 26) {
+ return item.getNumericModifiers();
+ }
+ return 0;
}
/**
@@ -616,8 +462,8 @@
public static void setAlphabeticShortcut(MenuItem item, char alphaChar, int alphaModifiers) {
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setAlphabeticShortcut(alphaChar, alphaModifiers);
- } else {
- IMPL.setAlphabeticShortcut(item, alphaChar, alphaModifiers);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setAlphabeticShortcut(alphaChar, alphaModifiers);
}
}
@@ -635,7 +481,10 @@
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getAlphabeticModifiers();
}
- return IMPL.getAlphabeticModifiers(item);
+ if (Build.VERSION.SDK_INT >= 26) {
+ return item.getAlphabeticModifiers();
+ }
+ return 0;
}
/**
@@ -653,8 +502,8 @@
public static void setIconTintList(MenuItem item, ColorStateList tint) {
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setIconTintList(tint);
- } else {
- IMPL.setIconTintList(item, tint);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setIconTintList(tint);
}
}
@@ -666,7 +515,10 @@
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getIconTintList();
}
- return IMPL.getIconTintList(item);
+ if (Build.VERSION.SDK_INT >= 26) {
+ return item.getIconTintList();
+ }
+ return null;
}
/**
@@ -681,8 +533,8 @@
public static void setIconTintMode(MenuItem item, PorterDuff.Mode tintMode) {
if (item instanceof SupportMenuItem) {
((SupportMenuItem) item).setIconTintMode(tintMode);
- } else {
- IMPL.setIconTintMode(item, tintMode);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ item.setIconTintMode(tintMode);
}
}
@@ -696,7 +548,10 @@
if (item instanceof SupportMenuItem) {
return ((SupportMenuItem) item).getIconTintMode();
}
- return IMPL.getIconTintMode(item);
+ if (Build.VERSION.SDK_INT >= 26) {
+ return item.getIconTintMode();
+ }
+ return null;
}
private MenuItemCompat() {}
diff --git a/compat/src/main/java/androidx/core/view/ViewCompat.java b/compat/src/main/java/androidx/core/view/ViewCompat.java
index 2ed252a..60e5f56 100644
--- a/compat/src/main/java/androidx/core/view/ViewCompat.java
+++ b/compat/src/main/java/androidx/core/view/ViewCompat.java
@@ -51,7 +51,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
-import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
@@ -452,220 +451,26 @@
private static Method sDispatchFinishTemporaryDetach;
private static boolean sTempDetachBound;
- static class ViewCompatBaseImpl {
- private static WeakHashMap<View, String> sTransitionNameMap;
- WeakHashMap<View, ViewPropertyAnimatorCompat> mViewPropertyAnimatorCompatMap = null;
- private static Method sChildrenDrawingOrderMethod;
- static Field sAccessibilityDelegateField;
- static boolean sAccessibilityDelegateCheckFailed = false;
+ private static WeakHashMap<View, String> sTransitionNameMap;
+ private static WeakHashMap<View, ViewPropertyAnimatorCompat> sViewPropertyAnimatorMap = null;
- public void setAccessibilityDelegate(View v,
- @Nullable AccessibilityDelegateCompat delegate) {
- v.setAccessibilityDelegate(delegate == null ? null : delegate.getBridge());
+ private static Method sChildrenDrawingOrderMethod;
+ private static Field sAccessibilityDelegateField;
+ private static boolean sAccessibilityDelegateCheckFailed = false;
+
+ private static ThreadLocal<Rect> sThreadLocalRect;
+
+ private static Rect getEmptyTempRect() {
+ if (sThreadLocalRect == null) {
+ sThreadLocalRect = new ThreadLocal<>();
}
-
- public boolean hasAccessibilityDelegate(View v) {
- if (sAccessibilityDelegateCheckFailed) {
- return false; // View implementation might have changed.
- }
- if (sAccessibilityDelegateField == null) {
- try {
- sAccessibilityDelegateField = View.class
- .getDeclaredField("mAccessibilityDelegate");
- sAccessibilityDelegateField.setAccessible(true);
- } catch (Throwable t) {
- sAccessibilityDelegateCheckFailed = true;
- return false;
- }
- }
- try {
- return sAccessibilityDelegateField.get(v) != null;
- } catch (Throwable t) {
- sAccessibilityDelegateCheckFailed = true;
- return false;
- }
+ Rect rect = sThreadLocalRect.get();
+ if (rect == null) {
+ rect = new Rect();
+ sThreadLocalRect.set(rect);
}
-
- public ViewPropertyAnimatorCompat animate(View view) {
- if (mViewPropertyAnimatorCompatMap == null) {
- mViewPropertyAnimatorCompatMap = new WeakHashMap<>();
- }
- ViewPropertyAnimatorCompat vpa = mViewPropertyAnimatorCompatMap.get(view);
- if (vpa == null) {
- vpa = new ViewPropertyAnimatorCompat(view);
- mViewPropertyAnimatorCompatMap.put(view, vpa);
- }
- return vpa;
- }
-
- public void setTransitionName(View view, String transitionName) {
- if (sTransitionNameMap == null) {
- sTransitionNameMap = new WeakHashMap<>();
- }
- sTransitionNameMap.put(view, transitionName);
- }
-
- public String getTransitionName(View view) {
- if (sTransitionNameMap == null) {
- return null;
- }
- return sTransitionNameMap.get(view);
- }
-
- public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) {
- if (sChildrenDrawingOrderMethod == null) {
- try {
- sChildrenDrawingOrderMethod = ViewGroup.class
- .getDeclaredMethod("setChildrenDrawingOrderEnabled", boolean.class);
- } catch (NoSuchMethodException e) {
- Log.e(TAG, "Unable to find childrenDrawingOrderEnabled", e);
- }
- sChildrenDrawingOrderMethod.setAccessible(true);
- }
- try {
- sChildrenDrawingOrderMethod.invoke(viewGroup, enabled);
- } catch (IllegalAccessException e) {
- Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e);
- } catch (InvocationTargetException e) {
- Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e);
- }
- }
-
- public void offsetLeftAndRight(View view, int offset) {
- view.offsetLeftAndRight(offset);
- if (view.getVisibility() == View.VISIBLE) {
- tickleInvalidationFlag(view);
-
- ViewParent parent = view.getParent();
- if (parent instanceof View) {
- tickleInvalidationFlag((View) parent);
- }
- }
- }
-
- public void offsetTopAndBottom(View view, int offset) {
- view.offsetTopAndBottom(offset);
- if (view.getVisibility() == View.VISIBLE) {
- tickleInvalidationFlag(view);
-
- ViewParent parent = view.getParent();
- if (parent instanceof View) {
- tickleInvalidationFlag((View) parent);
- }
- }
- }
-
- private static void tickleInvalidationFlag(View view) {
- final float y = view.getTranslationY();
- view.setTranslationY(y + 1);
- view.setTranslationY(y);
- }
- }
-
- @RequiresApi(21)
- static class ViewCompatApi21Impl extends ViewCompatBaseImpl {
- private static ThreadLocal<Rect> sThreadLocalRect;
-
- @Override
- public void setTransitionName(View view, String transitionName) {
- view.setTransitionName(transitionName);
- }
-
- @Override
- public String getTransitionName(View view) {
- return view.getTransitionName();
- }
-
- @Override
- public void offsetLeftAndRight(View view, int offset) {
- final Rect parentRect = getEmptyTempRect();
- boolean needInvalidateWorkaround = false;
-
- final ViewParent parent = view.getParent();
- if (parent instanceof View) {
- final View p = (View) parent;
- parentRect.set(p.getLeft(), p.getTop(), p.getRight(), p.getBottom());
- // If the view currently does not currently intersect the parent (and is therefore
- // not displayed) we may need need to invalidate
- needInvalidateWorkaround = !parentRect.intersects(view.getLeft(), view.getTop(),
- view.getRight(), view.getBottom());
- }
-
- // Now offset, invoking the API 11+ implementation (which contains its own workarounds)
- super.offsetLeftAndRight(view, offset);
-
- // The view has now been offset, so let's intersect the Rect and invalidate where
- // the View is now displayed
- if (needInvalidateWorkaround && parentRect.intersect(view.getLeft(), view.getTop(),
- view.getRight(), view.getBottom())) {
- ((View) parent).invalidate(parentRect);
- }
- }
-
- @Override
- public void offsetTopAndBottom(View view, int offset) {
- final Rect parentRect = getEmptyTempRect();
- boolean needInvalidateWorkaround = false;
-
- final ViewParent parent = view.getParent();
- if (parent instanceof View) {
- final View p = (View) parent;
- parentRect.set(p.getLeft(), p.getTop(), p.getRight(), p.getBottom());
- // If the view currently does not currently intersect the parent (and is therefore
- // not displayed) we may need need to invalidate
- needInvalidateWorkaround = !parentRect.intersects(view.getLeft(), view.getTop(),
- view.getRight(), view.getBottom());
- }
-
- // Now offset, invoking the API 11+ implementation (which contains its own workarounds)
- super.offsetTopAndBottom(view, offset);
-
- // The view has now been offset, so let's intersect the Rect and invalidate where
- // the View is now displayed
- if (needInvalidateWorkaround && parentRect.intersect(view.getLeft(), view.getTop(),
- view.getRight(), view.getBottom())) {
- ((View) parent).invalidate(parentRect);
- }
- }
-
- private static Rect getEmptyTempRect() {
- if (sThreadLocalRect == null) {
- sThreadLocalRect = new ThreadLocal<>();
- }
- Rect rect = sThreadLocalRect.get();
- if (rect == null) {
- rect = new Rect();
- sThreadLocalRect.set(rect);
- }
- rect.setEmpty();
- return rect;
- }
- }
-
- @RequiresApi(23)
- static class ViewCompatApi23Impl extends ViewCompatApi21Impl {
- @Override
- public void offsetLeftAndRight(View view, int offset) {
- view.offsetLeftAndRight(offset);
- }
-
- @Override
- public void offsetTopAndBottom(View view, int offset) {
- view.offsetTopAndBottom(offset);
- }
- }
-
- static final ViewCompatBaseImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 23) {
- IMPL = new ViewCompatApi23Impl();
- } else if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new ViewCompatApi21Impl();
- } else {
- IMPL = new ViewCompatBaseImpl();
- }
+ rect.setEmpty();
+ return rect;
}
/**
@@ -859,7 +664,7 @@
*/
public static void setAccessibilityDelegate(@NonNull View v,
AccessibilityDelegateCompat delegate) {
- IMPL.setAccessibilityDelegate(v, delegate);
+ v.setAccessibilityDelegate(delegate == null ? null : delegate.getBridge());
}
/**
@@ -1039,7 +844,25 @@
* @return True if the View has an accessibility delegate
*/
public static boolean hasAccessibilityDelegate(@NonNull View v) {
- return IMPL.hasAccessibilityDelegate(v);
+ if (sAccessibilityDelegateCheckFailed) {
+ return false; // View implementation might have changed.
+ }
+ if (sAccessibilityDelegateField == null) {
+ try {
+ sAccessibilityDelegateField = View.class
+ .getDeclaredField("mAccessibilityDelegate");
+ sAccessibilityDelegateField.setAccessible(true);
+ } catch (Throwable t) {
+ sAccessibilityDelegateCheckFailed = true;
+ return false;
+ }
+ }
+ try {
+ return sAccessibilityDelegateField.get(v) != null;
+ } catch (Throwable t) {
+ sAccessibilityDelegateCheckFailed = true;
+ return false;
+ }
}
/**
@@ -1167,7 +990,7 @@
if (Build.VERSION.SDK_INT >= 16) {
return view.getImportantForAccessibility();
}
- return 0;
+ return IMPORTANT_FOR_ACCESSIBILITY_AUTO;
}
/**
@@ -1915,7 +1738,15 @@
*/
@NonNull
public static ViewPropertyAnimatorCompat animate(@NonNull View view) {
- return IMPL.animate(view);
+ if (sViewPropertyAnimatorMap == null) {
+ sViewPropertyAnimatorMap = new WeakHashMap<>();
+ }
+ ViewPropertyAnimatorCompat vpa = sViewPropertyAnimatorMap.get(view);
+ if (vpa == null) {
+ vpa = new ViewPropertyAnimatorCompat(view);
+ sViewPropertyAnimatorMap.put(view, vpa);
+ }
+ return vpa;
}
/**
@@ -2226,7 +2057,14 @@
* @param transitionName The name of the View to uniquely identify it for Transitions.
*/
public static void setTransitionName(@NonNull View view, String transitionName) {
- IMPL.setTransitionName(view, transitionName);
+ if (Build.VERSION.SDK_INT >= 21) {
+ view.setTransitionName(transitionName);
+ } else {
+ if (sTransitionNameMap == null) {
+ sTransitionNameMap = new WeakHashMap<>();
+ }
+ sTransitionNameMap.put(view, transitionName);
+ }
}
/**
@@ -2241,7 +2079,13 @@
*/
@Nullable
public static String getTransitionName(@NonNull View view) {
- return IMPL.getTransitionName(view);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return view.getTransitionName();
+ }
+ if (sTransitionNameMap == null) {
+ return null;
+ }
+ return sTransitionNameMap.get(view);
}
/**
@@ -2279,7 +2123,24 @@
*/
@Deprecated
public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) {
- IMPL.setChildrenDrawingOrderEnabled(viewGroup, enabled);
+ if (sChildrenDrawingOrderMethod == null) {
+ try {
+ sChildrenDrawingOrderMethod = ViewGroup.class
+ .getDeclaredMethod("setChildrenDrawingOrderEnabled", boolean.class);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Unable to find childrenDrawingOrderEnabled", e);
+ }
+ sChildrenDrawingOrderMethod.setAccessible(true);
+ }
+ try {
+ sChildrenDrawingOrderMethod.invoke(viewGroup, enabled);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e);
+ } catch (InvocationTargetException e) {
+ Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e);
+ }
}
/**
@@ -3039,7 +2900,46 @@
* @param offset the number of pixels to offset the view by
*/
public static void offsetTopAndBottom(@NonNull View view, int offset) {
- IMPL.offsetTopAndBottom(view, offset);
+ if (Build.VERSION.SDK_INT >= 23) {
+ view.offsetTopAndBottom(offset);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ final Rect parentRect = getEmptyTempRect();
+ boolean needInvalidateWorkaround = false;
+
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View p = (View) parent;
+ parentRect.set(p.getLeft(), p.getTop(), p.getRight(), p.getBottom());
+ // If the view currently does not currently intersect the parent (and is therefore
+ // not displayed) we may need need to invalidate
+ needInvalidateWorkaround = !parentRect.intersects(view.getLeft(), view.getTop(),
+ view.getRight(), view.getBottom());
+ }
+
+ // Now offset, invoking the API 14+ implementation (which contains its own workarounds)
+ compatOffsetTopAndBottom(view, offset);
+
+ // The view has now been offset, so let's intersect the Rect and invalidate where
+ // the View is now displayed
+ if (needInvalidateWorkaround && parentRect.intersect(view.getLeft(), view.getTop(),
+ view.getRight(), view.getBottom())) {
+ ((View) parent).invalidate(parentRect);
+ }
+ } else {
+ compatOffsetTopAndBottom(view, offset);
+ }
+ }
+
+ private static void compatOffsetTopAndBottom(View view, int offset) {
+ view.offsetTopAndBottom(offset);
+ if (view.getVisibility() == View.VISIBLE) {
+ tickleInvalidationFlag(view);
+
+ ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ tickleInvalidationFlag((View) parent);
+ }
+ }
}
/**
@@ -3048,7 +2948,52 @@
* @param offset the number of pixels to offset the view by
*/
public static void offsetLeftAndRight(@NonNull View view, int offset) {
- IMPL.offsetLeftAndRight(view, offset);
+ if (Build.VERSION.SDK_INT >= 23) {
+ view.offsetLeftAndRight(offset);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ final Rect parentRect = getEmptyTempRect();
+ boolean needInvalidateWorkaround = false;
+
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View p = (View) parent;
+ parentRect.set(p.getLeft(), p.getTop(), p.getRight(), p.getBottom());
+ // If the view currently does not currently intersect the parent (and is therefore
+ // not displayed) we may need need to invalidate
+ needInvalidateWorkaround = !parentRect.intersects(view.getLeft(), view.getTop(),
+ view.getRight(), view.getBottom());
+ }
+
+ // Now offset, invoking the API 14+ implementation (which contains its own workarounds)
+ compatOffsetLeftAndRight(view, offset);
+
+ // The view has now been offset, so let's intersect the Rect and invalidate where
+ // the View is now displayed
+ if (needInvalidateWorkaround && parentRect.intersect(view.getLeft(), view.getTop(),
+ view.getRight(), view.getBottom())) {
+ ((View) parent).invalidate(parentRect);
+ }
+ } else {
+ compatOffsetLeftAndRight(view, offset);
+ }
+ }
+
+ private static void compatOffsetLeftAndRight(View view, int offset) {
+ view.offsetLeftAndRight(offset);
+ if (view.getVisibility() == View.VISIBLE) {
+ tickleInvalidationFlag(view);
+
+ ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ tickleInvalidationFlag((View) parent);
+ }
+ }
+ }
+
+ private static void tickleInvalidationFlag(View view) {
+ final float y = view.getTranslationY();
+ view.setTranslationY(y + 1);
+ view.setTranslationY(y);
}
/**
diff --git a/compat/src/main/java/androidx/core/view/ViewGroupCompat.java b/compat/src/main/java/androidx/core/view/ViewGroupCompat.java
index bd1d0ed..7da7247 100644
--- a/compat/src/main/java/androidx/core/view/ViewGroupCompat.java
+++ b/compat/src/main/java/androidx/core/view/ViewGroupCompat.java
@@ -22,7 +22,6 @@
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.core.R;
import androidx.core.view.ViewCompat.ScrollAxis;
@@ -47,76 +46,6 @@
*/
public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1;
- static class ViewGroupCompatBaseImpl {
- public int getLayoutMode(ViewGroup group) {
- return LAYOUT_MODE_CLIP_BOUNDS;
- }
-
- public void setLayoutMode(ViewGroup group, int mode) {
- // no-op, didn't exist. Views only support clip bounds.
- }
-
- public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) {
- group.setTag(R.id.tag_transition_group, isTransitionGroup);
- }
-
- public boolean isTransitionGroup(ViewGroup group) {
- Boolean explicit = (Boolean) group.getTag(R.id.tag_transition_group);
- return (explicit != null && explicit)
- || group.getBackground() != null
- || ViewCompat.getTransitionName(group) != null;
- }
-
- public int getNestedScrollAxes(ViewGroup group) {
- if (group instanceof NestedScrollingParent) {
- return ((NestedScrollingParent) group).getNestedScrollAxes();
- }
- return ViewCompat.SCROLL_AXIS_NONE;
- }
- }
-
- @RequiresApi(18)
- static class ViewGroupCompatApi18Impl extends ViewGroupCompatBaseImpl {
- @Override
- public int getLayoutMode(ViewGroup group) {
- return group.getLayoutMode();
- }
-
- @Override
- public void setLayoutMode(ViewGroup group, int mode) {
- group.setLayoutMode(mode);
- }
- }
-
- @RequiresApi(21)
- static class ViewGroupCompatApi21Impl extends ViewGroupCompatApi18Impl {
- @Override
- public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) {
- group.setTransitionGroup(isTransitionGroup);
- }
-
- @Override
- public boolean isTransitionGroup(ViewGroup group) {
- return group.isTransitionGroup();
- }
-
- @Override
- public int getNestedScrollAxes(ViewGroup group) {
- return group.getNestedScrollAxes();
- }
- }
-
- static final ViewGroupCompatBaseImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new ViewGroupCompatApi21Impl();
- } else if (Build.VERSION.SDK_INT >= 18) {
- IMPL = new ViewGroupCompatApi18Impl();
- } else {
- IMPL = new ViewGroupCompatBaseImpl();
- }
- }
-
/*
* Hide the constructor.
*/
@@ -182,7 +111,10 @@
* @see #setLayoutMode(ViewGroup, int)
*/
public static int getLayoutMode(@NonNull ViewGroup group) {
- return IMPL.getLayoutMode(group);
+ if (Build.VERSION.SDK_INT >= 18) {
+ return group.getLayoutMode();
+ }
+ return LAYOUT_MODE_CLIP_BOUNDS;
}
/**
@@ -195,7 +127,9 @@
* @see #getLayoutMode(ViewGroup)
*/
public static void setLayoutMode(@NonNull ViewGroup group, int mode) {
- IMPL.setLayoutMode(group, mode);
+ if (Build.VERSION.SDK_INT >= 18) {
+ group.setLayoutMode(mode);
+ }
}
/**
@@ -207,7 +141,11 @@
* together.
*/
public static void setTransitionGroup(@NonNull ViewGroup group, boolean isTransitionGroup) {
- IMPL.setTransitionGroup(group, isTransitionGroup);
+ if (Build.VERSION.SDK_INT >= 21) {
+ group.setTransitionGroup(isTransitionGroup);
+ } else {
+ group.setTag(R.id.tag_transition_group, isTransitionGroup);
+ }
}
/**
@@ -216,7 +154,13 @@
* individually during the transition.
*/
public static boolean isTransitionGroup(@NonNull ViewGroup group) {
- return IMPL.isTransitionGroup(group);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return group.isTransitionGroup();
+ }
+ Boolean explicit = (Boolean) group.getTag(R.id.tag_transition_group);
+ return (explicit != null && explicit)
+ || group.getBackground() != null
+ || ViewCompat.getTransitionName(group) != null;
}
/**
@@ -232,7 +176,14 @@
* @see ViewCompat#SCROLL_AXIS_NONE
*/
@ScrollAxis
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static int getNestedScrollAxes(@NonNull ViewGroup group) {
- return IMPL.getNestedScrollAxes(group);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return group.getNestedScrollAxes();
+ }
+ if (group instanceof NestedScrollingParent) {
+ return ((NestedScrollingParent) group).getNestedScrollAxes();
+ }
+ return ViewCompat.SCROLL_AXIS_NONE;
}
}
diff --git a/compat/src/main/java/androidx/core/view/ViewParentCompat.java b/compat/src/main/java/androidx/core/view/ViewParentCompat.java
index 1786ee3..5ad38cc 100644
--- a/compat/src/main/java/androidx/core/view/ViewParentCompat.java
+++ b/compat/src/main/java/androidx/core/view/ViewParentCompat.java
@@ -25,8 +25,6 @@
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
-import androidx.annotation.RequiresApi;
-
/**
* Helper for accessing features in {@link ViewParent}.
*/
@@ -34,171 +32,6 @@
private static final String TAG = "ViewParentCompat";
- static class ViewParentCompatBaseImpl {
- public boolean onStartNestedScroll(ViewParent parent, View child, View target,
- int nestedScrollAxes) {
- if (parent instanceof NestedScrollingParent) {
- return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
- nestedScrollAxes);
- }
- return false;
- }
-
- public void onNestedScrollAccepted(ViewParent parent, View child, View target,
- int nestedScrollAxes) {
- if (parent instanceof NestedScrollingParent) {
- ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
- nestedScrollAxes);
- }
- }
-
- public void onStopNestedScroll(ViewParent parent, View target) {
- if (parent instanceof NestedScrollingParent) {
- ((NestedScrollingParent) parent).onStopNestedScroll(target);
- }
- }
-
- public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed) {
- if (parent instanceof NestedScrollingParent) {
- ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed,
- dxUnconsumed, dyUnconsumed);
- }
- }
-
- public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
- int[] consumed) {
- if (parent instanceof NestedScrollingParent) {
- ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
- }
- }
-
- public boolean onNestedFling(ViewParent parent, View target, float velocityX,
- float velocityY, boolean consumed) {
- if (parent instanceof NestedScrollingParent) {
- return ((NestedScrollingParent) parent).onNestedFling(target, velocityX, velocityY,
- consumed);
- }
- return false;
- }
-
- public boolean onNestedPreFling(ViewParent parent, View target, float velocityX,
- float velocityY) {
- if (parent instanceof NestedScrollingParent) {
- return ((NestedScrollingParent) parent).onNestedPreFling(target, velocityX,
- velocityY);
- }
- return false;
- }
-
- public void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child,
- View source, int changeType) {
- }
- }
-
- @RequiresApi(19)
- static class ViewParentCompatApi19Impl extends ViewParentCompatBaseImpl {
-
- @Override
- public void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child,
- View source, int changeType) {
- parent.notifySubtreeAccessibilityStateChanged(child, source, changeType);
- }
- }
-
- @RequiresApi(21)
- static class ViewParentCompatApi21Impl extends ViewParentCompatApi19Impl {
- @Override
- public boolean onStartNestedScroll(ViewParent parent, View child, View target,
- int nestedScrollAxes) {
- try {
- return parent.onStartNestedScroll(child, target, nestedScrollAxes);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onStartNestedScroll", e);
- return false;
- }
- }
-
- @Override
- public void onNestedScrollAccepted(ViewParent parent, View child, View target,
- int nestedScrollAxes) {
- try {
- parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onNestedScrollAccepted", e);
- }
- }
-
- @Override
- public void onStopNestedScroll(ViewParent parent, View target) {
- try {
- parent.onStopNestedScroll(target);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onStopNestedScroll", e);
- }
- }
-
- @Override
- public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed,
- int dxUnconsumed, int dyUnconsumed) {
- try {
- parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onNestedScroll", e);
- }
- }
-
- @Override
- public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
- int[] consumed) {
- try {
- parent.onNestedPreScroll(target, dx, dy, consumed);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onNestedPreScroll", e);
- }
- }
-
- @Override
- public boolean onNestedFling(ViewParent parent, View target, float velocityX,
- float velocityY, boolean consumed) {
- try {
- return parent.onNestedFling(target, velocityX, velocityY, consumed);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onNestedFling", e);
- return false;
- }
- }
-
- @Override
- public boolean onNestedPreFling(ViewParent parent, View target, float velocityX,
- float velocityY) {
- try {
- return parent.onNestedPreFling(target, velocityX, velocityY);
- } catch (AbstractMethodError e) {
- Log.e(TAG, "ViewParent " + parent + " does not implement interface "
- + "method onNestedPreFling", e);
- return false;
- }
- }
- }
-
- static final ViewParentCompatBaseImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new ViewParentCompatApi21Impl();
- } else if (Build.VERSION.SDK_INT >= 19) {
- IMPL = new ViewParentCompatApi19Impl();
- } else {
- IMPL = new ViewParentCompatBaseImpl();
- }
- }
-
/*
* Hide the constructor.
*/
@@ -337,6 +170,7 @@
* @param type the type of input which cause this scroll event
* @return true if this ViewParent accepts the nested scroll operation
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
@@ -345,7 +179,17 @@
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
- return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ return parent.onStartNestedScroll(child, target, nestedScrollAxes);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onStartNestedScroll", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
+ nestedScrollAxes);
+ }
}
return false;
}
@@ -367,6 +211,7 @@
* @see #onStartNestedScroll(ViewParent, View, View, int)
* @see #onStopNestedScroll(ViewParent, View, int)
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void onNestedScrollAccepted(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
@@ -375,7 +220,17 @@
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
- IMPL.onNestedScrollAccepted(parent, child, target, nestedScrollAxes);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onNestedScrollAccepted", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target,
+ nestedScrollAxes);
+ }
}
}
@@ -391,13 +246,23 @@
* @param target View that initiated the nested scroll
* @param type the type of input which cause this scroll event
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void onStopNestedScroll(ViewParent parent, View target, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onStopNestedScroll(target, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
- IMPL.onStopNestedScroll(parent, target);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ parent.onStopNestedScroll(target);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onStopNestedScroll", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ ((NestedScrollingParent) parent).onStopNestedScroll(target);
+ }
}
}
@@ -423,6 +288,7 @@
* @param dyUnconsumed Vertical scroll distance in pixels not consumed by target
* @param type the type of input which cause this scroll event
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (parent instanceof NestedScrollingParent2) {
@@ -431,7 +297,18 @@
dxUnconsumed, dyUnconsumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
- IMPL.onNestedScroll(parent, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed,
+ dyUnconsumed);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onNestedScroll", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed,
+ dxUnconsumed, dyUnconsumed);
+ }
}
}
@@ -456,6 +333,7 @@
* @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
* @param type the type of input which cause this scroll event
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
@@ -463,7 +341,16 @@
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
- IMPL.onNestedPreScroll(parent, target, dx, dy, consumed);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ parent.onNestedPreScroll(target, dx, dy, consumed);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onNestedPreScroll", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
+ }
}
}
@@ -486,9 +373,21 @@
* @param consumed true if the child consumed the fling, false otherwise
* @return true if this parent consumed or otherwise reacted to the fling
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static boolean onNestedFling(ViewParent parent, View target, float velocityX,
float velocityY, boolean consumed) {
- return IMPL.onNestedFling(parent, target, velocityX, velocityY, consumed);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ return parent.onNestedFling(target, velocityX, velocityY, consumed);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onNestedFling", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ return ((NestedScrollingParent) parent).onNestedFling(target, velocityX, velocityY,
+ consumed);
+ }
+ return false;
}
/**
@@ -511,9 +410,21 @@
* @param velocityY Vertical velocity in pixels per second
* @return true if this parent consumed the fling ahead of the target view
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static boolean onNestedPreFling(ViewParent parent, View target, float velocityX,
float velocityY) {
- return IMPL.onNestedPreFling(parent, target, velocityX, velocityY);
+ if (Build.VERSION.SDK_INT >= 21) {
+ try {
+ return parent.onNestedPreFling(target, velocityX, velocityY);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ + "method onNestedPreFling", e);
+ }
+ } else if (parent instanceof NestedScrollingParent) {
+ return ((NestedScrollingParent) parent).onNestedPreFling(target, velocityX,
+ velocityY);
+ }
+ return false;
}
/**
@@ -533,6 +444,8 @@
*/
public static void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child,
View source, int changeType) {
- IMPL.notifySubtreeAccessibilityStateChanged(parent, child, source, changeType);
+ if (Build.VERSION.SDK_INT >= 19) {
+ parent.notifySubtreeAccessibilityStateChanged(child, source, changeType);
+ }
}
}
diff --git a/compat/src/main/java/androidx/core/widget/CompoundButtonCompat.java b/compat/src/main/java/androidx/core/widget/CompoundButtonCompat.java
index 519f98a..ccbf0df 100644
--- a/compat/src/main/java/androidx/core/widget/CompoundButtonCompat.java
+++ b/compat/src/main/java/androidx/core/widget/CompoundButtonCompat.java
@@ -25,7 +25,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.DrawableCompat;
import java.lang.reflect.Field;
@@ -34,104 +33,10 @@
* Helper for accessing {@link android.widget.CompoundButton}.
*/
public final class CompoundButtonCompat {
+ private static final String TAG = "CompoundButtonCompat";
- private static final CompoundButtonCompatBaseImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 23) {
- IMPL = new CompoundButtonCompatApi23Impl();
- } else if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new CompoundButtonCompatApi21Impl();
- } else {
- IMPL = new CompoundButtonCompatBaseImpl();
- }
- }
-
- static class CompoundButtonCompatBaseImpl {
- private static final String TAG = "CompoundButtonCompat";
-
- private static Field sButtonDrawableField;
- private static boolean sButtonDrawableFieldFetched;
-
- public void setButtonTintList(CompoundButton button, ColorStateList tint) {
- if (button instanceof TintableCompoundButton) {
- ((TintableCompoundButton) button).setSupportButtonTintList(tint);
- }
- }
-
- public ColorStateList getButtonTintList(CompoundButton button) {
- if (button instanceof TintableCompoundButton) {
- return ((TintableCompoundButton) button).getSupportButtonTintList();
- }
- return null;
- }
-
- public void setButtonTintMode(CompoundButton button, PorterDuff.Mode tintMode) {
- if (button instanceof TintableCompoundButton) {
- ((TintableCompoundButton) button).setSupportButtonTintMode(tintMode);
- }
- }
-
- public PorterDuff.Mode getButtonTintMode(CompoundButton button) {
- if (button instanceof TintableCompoundButton) {
- return ((TintableCompoundButton) button).getSupportButtonTintMode();
- }
- return null;
- }
-
- public Drawable getButtonDrawable(CompoundButton button) {
- if (!sButtonDrawableFieldFetched) {
- try {
- sButtonDrawableField = CompoundButton.class.getDeclaredField("mButtonDrawable");
- sButtonDrawableField.setAccessible(true);
- } catch (NoSuchFieldException e) {
- Log.i(TAG, "Failed to retrieve mButtonDrawable field", e);
- }
- sButtonDrawableFieldFetched = true;
- }
-
- if (sButtonDrawableField != null) {
- try {
- return (Drawable) sButtonDrawableField.get(button);
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Failed to get button drawable via reflection", e);
- sButtonDrawableField = null;
- }
- }
- return null;
- }
- }
-
- @RequiresApi(21)
- static class CompoundButtonCompatApi21Impl extends CompoundButtonCompatBaseImpl {
- @Override
- public void setButtonTintList(CompoundButton button, ColorStateList tint) {
- button.setButtonTintList(tint);
- }
-
- @Override
- public ColorStateList getButtonTintList(CompoundButton button) {
- return button.getButtonTintList();
- }
-
- @Override
- public void setButtonTintMode(CompoundButton button, PorterDuff.Mode tintMode) {
- button.setButtonTintMode(tintMode);
- }
-
- @Override
- public PorterDuff.Mode getButtonTintMode(CompoundButton button) {
- return button.getButtonTintMode();
- }
- }
-
- @RequiresApi(23)
- static class CompoundButtonCompatApi23Impl extends CompoundButtonCompatApi21Impl {
- @Override
- public Drawable getButtonDrawable(CompoundButton button) {
- return button.getButtonDrawable();
- }
- }
+ private static Field sButtonDrawableField;
+ private static boolean sButtonDrawableFieldFetched;
private CompoundButtonCompat() {}
@@ -147,8 +52,13 @@
*
* @see #setButtonTintList(CompoundButton, ColorStateList)
*/
- public static void setButtonTintList(@NonNull CompoundButton button, @Nullable ColorStateList tint) {
- IMPL.setButtonTintList(button, tint);
+ public static void setButtonTintList(@NonNull CompoundButton button,
+ @Nullable ColorStateList tint) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ button.setButtonTintList(tint);
+ } else if (button instanceof TintableCompoundButton) {
+ ((TintableCompoundButton) button).setSupportButtonTintList(tint);
+ }
}
/**
@@ -158,7 +68,13 @@
*/
@Nullable
public static ColorStateList getButtonTintList(@NonNull CompoundButton button) {
- return IMPL.getButtonTintList(button);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return button.getButtonTintList();
+ }
+ if (button instanceof TintableCompoundButton) {
+ return ((TintableCompoundButton) button).getSupportButtonTintList();
+ }
+ return null;
}
/**
@@ -174,7 +90,11 @@
*/
public static void setButtonTintMode(@NonNull CompoundButton button,
@Nullable PorterDuff.Mode tintMode) {
- IMPL.setButtonTintMode(button, tintMode);
+ if (Build.VERSION.SDK_INT >= 21) {
+ button.setButtonTintMode(tintMode);
+ } else if (button instanceof TintableCompoundButton) {
+ ((TintableCompoundButton) button).setSupportButtonTintMode(tintMode);
+ }
}
/**
@@ -184,7 +104,13 @@
*/
@Nullable
public static PorterDuff.Mode getButtonTintMode(@NonNull CompoundButton button) {
- return IMPL.getButtonTintMode(button);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return button.getButtonTintMode();
+ }
+ if (button instanceof TintableCompoundButton) {
+ return ((TintableCompoundButton) button).getSupportButtonTintMode();
+ }
+ return null;
}
/**
@@ -194,6 +120,28 @@
*/
@Nullable
public static Drawable getButtonDrawable(@NonNull CompoundButton button) {
- return IMPL.getButtonDrawable(button);
+ if (Build.VERSION.SDK_INT >= 23) {
+ return button.getButtonDrawable();
+ }
+
+ if (!sButtonDrawableFieldFetched) {
+ try {
+ sButtonDrawableField = CompoundButton.class.getDeclaredField("mButtonDrawable");
+ sButtonDrawableField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "Failed to retrieve mButtonDrawable field", e);
+ }
+ sButtonDrawableFieldFetched = true;
+ }
+
+ if (sButtonDrawableField != null) {
+ try {
+ return (Drawable) sButtonDrawableField.get(button);
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Failed to get button drawable via reflection", e);
+ sButtonDrawableField = null;
+ }
+ }
+ return null;
}
}
diff --git a/compat/src/main/java/androidx/core/widget/ImageViewCompat.java b/compat/src/main/java/androidx/core/widget/ImageViewCompat.java
index ce93724..3ec5b88 100644
--- a/compat/src/main/java/androidx/core/widget/ImageViewCompat.java
+++ b/compat/src/main/java/androidx/core/widget/ImageViewCompat.java
@@ -24,61 +24,30 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
/**
* Helper for accessing features in {@link ImageView}.
*/
public class ImageViewCompat {
- interface ImageViewCompatImpl {
- ColorStateList getImageTintList(ImageView view);
-
- void setImageTintList(ImageView view, ColorStateList tintList);
-
- PorterDuff.Mode getImageTintMode(ImageView view);
-
- void setImageTintMode(ImageView view, PorterDuff.Mode mode);
- }
-
- static class BaseViewCompatImpl implements ImageViewCompatImpl {
- @Override
- public ColorStateList getImageTintList(ImageView view) {
- return (view instanceof TintableImageSourceView)
- ? ((TintableImageSourceView) view).getSupportImageTintList()
- : null;
- }
-
- @Override
- public void setImageTintList(ImageView view, ColorStateList tintList) {
- if (view instanceof TintableImageSourceView) {
- ((TintableImageSourceView) view).setSupportImageTintList(tintList);
- }
- }
-
- @Override
- public void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
- if (view instanceof TintableImageSourceView) {
- ((TintableImageSourceView) view).setSupportImageTintMode(mode);
- }
- }
-
- @Override
- public PorterDuff.Mode getImageTintMode(ImageView view) {
- return (view instanceof TintableImageSourceView)
- ? ((TintableImageSourceView) view).getSupportImageTintMode()
- : null;
- }
- }
-
- @RequiresApi(21)
- static class LollipopViewCompatImpl extends BaseViewCompatImpl {
- @Override
- public ColorStateList getImageTintList(ImageView view) {
+ /**
+ * Return the tint applied to the image drawable, if specified.
+ */
+ @Nullable
+ public static ColorStateList getImageTintList(@NonNull ImageView view) {
+ if (Build.VERSION.SDK_INT >= 21) {
return view.getImageTintList();
}
+ return (view instanceof TintableImageSourceView)
+ ? ((TintableImageSourceView) view).getSupportImageTintList()
+ : null;
+ }
- @Override
- public void setImageTintList(ImageView view, ColorStateList tintList) {
+ /**
+ * Applies a tint to the image drawable.
+ */
+ public static void setImageTintList(@NonNull ImageView view,
+ @Nullable ColorStateList tintList) {
+ if (Build.VERSION.SDK_INT >= 21) {
view.setImageTintList(tintList);
if (Build.VERSION.SDK_INT == 21) {
@@ -94,10 +63,31 @@
view.setImageDrawable(imageViewDrawable);
}
}
+ } else if (view instanceof TintableImageSourceView) {
+ ((TintableImageSourceView) view).setSupportImageTintList(tintList);
}
+ }
- @Override
- public void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+ /**
+ * Return the blending mode used to apply the tint to the image drawable, if specified.
+ */
+ @Nullable
+ public static PorterDuff.Mode getImageTintMode(@NonNull ImageView view) {
+ if (Build.VERSION.SDK_INT >= 21) {
+ return view.getImageTintMode();
+ }
+ return (view instanceof TintableImageSourceView)
+ ? ((TintableImageSourceView) view).getSupportImageTintMode()
+ : null;
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setImageTintList(android.widget.ImageView, android.content.res.ColorStateList)}
+ * to the image drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ */
+ public static void setImageTintMode(@NonNull ImageView view, @Nullable PorterDuff.Mode mode) {
+ if (Build.VERSION.SDK_INT >= 21) {
view.setImageTintMode(mode);
if (Build.VERSION.SDK_INT == 21) {
@@ -113,54 +103,9 @@
view.setImageDrawable(imageViewDrawable);
}
}
+ } else if (view instanceof TintableImageSourceView) {
+ ((TintableImageSourceView) view).setSupportImageTintMode(mode);
}
-
- @Override
- public PorterDuff.Mode getImageTintMode(ImageView view) {
- return view.getImageTintMode();
- }
- }
-
- static final ImageViewCompatImpl IMPL;
- static {
- if (android.os.Build.VERSION.SDK_INT >= 21) {
- IMPL = new LollipopViewCompatImpl();
- } else {
- IMPL = new BaseViewCompatImpl();
- }
- }
-
- /**
- * Return the tint applied to the image drawable, if specified.
- */
- @Nullable
- public static ColorStateList getImageTintList(@NonNull ImageView view) {
- return IMPL.getImageTintList(view);
- }
-
- /**
- * Applies a tint to the image drawable.
- */
- public static void setImageTintList(@NonNull ImageView view,
- @Nullable ColorStateList tintList) {
- IMPL.setImageTintList(view, tintList);
- }
-
- /**
- * Return the blending mode used to apply the tint to the image drawable, if specified.
- */
- @Nullable
- public static PorterDuff.Mode getImageTintMode(@NonNull ImageView view) {
- return IMPL.getImageTintMode(view);
- }
-
- /**
- * Specifies the blending mode used to apply the tint specified by
- * {@link #setImageTintList(android.widget.ImageView, android.content.res.ColorStateList)}
- * to the image drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
- */
- public static void setImageTintMode(@NonNull ImageView view, @Nullable PorterDuff.Mode mode) {
- IMPL.setImageTintMode(view, mode);
}
private ImageViewCompat() {}
diff --git a/compat/src/main/java/androidx/core/widget/NestedScrollView.java b/compat/src/main/java/androidx/core/widget/NestedScrollView.java
index 2490bd4..307f32f 100644
--- a/compat/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/compat/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -52,7 +52,7 @@
import androidx.core.view.InputDeviceCompat;
import androidx.core.view.NestedScrollingChild2;
import androidx.core.view.NestedScrollingChildHelper;
-import androidx.core.view.NestedScrollingParent;
+import androidx.core.view.NestedScrollingParent2;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ScrollingView;
import androidx.core.view.ViewCompat;
@@ -66,7 +66,7 @@
* as both a nested scrolling parent and child on both new and old versions of Android.
* Nested scrolling is enabled by default.
*/
-public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
+public class NestedScrollView extends FrameLayout implements NestedScrollingParent2,
NestedScrollingChild2, ScrollingView {
static final int ANIMATED_SCROLL_GAP = 250;
@@ -214,6 +214,36 @@
ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE);
}
+ // NestedScrollingChild2
+
+ @Override
+ public boolean startNestedScroll(int axes, int type) {
+ return mChildHelper.startNestedScroll(axes, type);
+ }
+
+ @Override
+ public void stopNestedScroll(int type) {
+ mChildHelper.stopNestedScroll(type);
+ }
+
+ @Override
+ public boolean hasNestedScrollingParent(int type) {
+ return mChildHelper.hasNestedScrollingParent(type);
+ }
+
+ @Override
+ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+ int dyUnconsumed, int[] offsetInWindow, int type) {
+ return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+ offsetInWindow, type);
+ }
+
+ @Override
+ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
+ int type) {
+ return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
+ }
+
// NestedScrollingChild
@Override
@@ -228,57 +258,29 @@
@Override
public boolean startNestedScroll(int axes) {
- return mChildHelper.startNestedScroll(axes);
- }
-
- @Override
- public boolean startNestedScroll(int axes, int type) {
- return mChildHelper.startNestedScroll(axes, type);
+ return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
}
@Override
public void stopNestedScroll() {
- mChildHelper.stopNestedScroll();
- }
-
- @Override
- public void stopNestedScroll(int type) {
- mChildHelper.stopNestedScroll(type);
+ stopNestedScroll(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean hasNestedScrollingParent() {
- return mChildHelper.hasNestedScrollingParent();
- }
-
- @Override
- public boolean hasNestedScrollingParent(int type) {
- return mChildHelper.hasNestedScrollingParent(type);
+ return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
- return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
- offsetInWindow);
- }
-
- @Override
- public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
- int dyUnconsumed, int[] offsetInWindow, int type) {
- return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
- offsetInWindow, type);
+ return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+ offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
- return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
- }
-
- @Override
- public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
- int type) {
- return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
+ return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
}
@Override
@@ -291,38 +293,71 @@
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
+ // NestedScrollingParent2
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
+ int type) {
+ return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
+ int type) {
+ mParentHelper.onNestedScrollAccepted(child, target, axes, type);
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
+ }
+
+ @Override
+ public void onStopNestedScroll(@NonNull View target, int type) {
+ mParentHelper.onStopNestedScroll(target, type);
+ stopNestedScroll(type);
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
+ int dyUnconsumed, int type) {
+ final int oldScrollY = getScrollY();
+ scrollBy(0, dyUnconsumed);
+ final int myConsumed = getScrollY() - oldScrollY;
+ final int myUnconsumed = dyUnconsumed - myConsumed;
+ dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null,
+ type);
+ }
+
+ @Override
+ public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
+ int type) {
+ dispatchNestedPreScroll(dx, dy, consumed, null, type);
+ }
+
// NestedScrollingParent
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
- return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
+ return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
- mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
- startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
+ onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}
@Override
public void onStopNestedScroll(View target) {
- mParentHelper.onStopNestedScroll(target);
- stopNestedScroll();
+ onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed) {
- final int oldScrollY = getScrollY();
- scrollBy(0, dyUnconsumed);
- final int myConsumed = getScrollY() - oldScrollY;
- final int myUnconsumed = dyUnconsumed - myConsumed;
- dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
+ onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+ ViewCompat.TYPE_TOUCH);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
- dispatchNestedPreScroll(dx, dy, consumed, null);
+ onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}
@Override
diff --git a/compat/src/main/java/androidx/core/widget/PopupWindowCompat.java b/compat/src/main/java/androidx/core/widget/PopupWindowCompat.java
index cc97ffa..6351055 100644
--- a/compat/src/main/java/androidx/core/widget/PopupWindowCompat.java
+++ b/compat/src/main/java/androidx/core/widget/PopupWindowCompat.java
@@ -23,7 +23,6 @@
import android.widget.PopupWindow;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
@@ -34,166 +33,15 @@
* Helper for accessing features in {@link PopupWindow}.
*/
public final class PopupWindowCompat {
+ private static final String TAG = "PopupWindowCompatApi21";
- static class PopupWindowCompatBaseImpl {
- private static Method sSetWindowLayoutTypeMethod;
- private static boolean sSetWindowLayoutTypeMethodAttempted;
- private static Method sGetWindowLayoutTypeMethod;
- private static boolean sGetWindowLayoutTypeMethodAttempted;
+ private static Method sSetWindowLayoutTypeMethod;
+ private static boolean sSetWindowLayoutTypeMethodAttempted;
+ private static Method sGetWindowLayoutTypeMethod;
+ private static boolean sGetWindowLayoutTypeMethodAttempted;
- public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff,
- int gravity) {
- final int hgrav = GravityCompat.getAbsoluteGravity(gravity,
- ViewCompat.getLayoutDirection(anchor)) & Gravity.HORIZONTAL_GRAVITY_MASK;
- if (hgrav == Gravity.RIGHT) {
- // Flip the location to align the right sides of the popup and
- // anchor instead of left.
- xoff -= (popup.getWidth() - anchor.getWidth());
- }
- popup.showAsDropDown(anchor, xoff, yoff);
- }
-
- public void setOverlapAnchor(PopupWindow popupWindow, boolean overlapAnchor) {
- // noop
- }
-
- public boolean getOverlapAnchor(PopupWindow popupWindow) {
- return false;
- }
-
- public void setWindowLayoutType(PopupWindow popupWindow, int layoutType) {
- if (!sSetWindowLayoutTypeMethodAttempted) {
- try {
- sSetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
- "setWindowLayoutType", int.class);
- sSetWindowLayoutTypeMethod.setAccessible(true);
- } catch (Exception e) {
- // Reflection method fetch failed. Oh well.
- }
- sSetWindowLayoutTypeMethodAttempted = true;
- }
-
- if (sSetWindowLayoutTypeMethod != null) {
- try {
- sSetWindowLayoutTypeMethod.invoke(popupWindow, layoutType);
- } catch (Exception e) {
- // Reflection call failed. Oh well.
- }
- }
- }
-
- public int getWindowLayoutType(PopupWindow popupWindow) {
- if (!sGetWindowLayoutTypeMethodAttempted) {
- try {
- sGetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
- "getWindowLayoutType");
- sGetWindowLayoutTypeMethod.setAccessible(true);
- } catch (Exception e) {
- // Reflection method fetch failed. Oh well.
- }
- sGetWindowLayoutTypeMethodAttempted = true;
- }
-
- if (sGetWindowLayoutTypeMethod != null) {
- try {
- return (Integer) sGetWindowLayoutTypeMethod.invoke(popupWindow);
- } catch (Exception e) {
- // Reflection call failed. Oh well.
- }
- }
- return 0;
- }
- }
-
- /**
- * Interface implementation for devices with at least KitKat APIs.
- */
- @RequiresApi(19)
- static class PopupWindowCompatApi19Impl extends PopupWindowCompatBaseImpl {
- @Override
- public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff,
- int gravity) {
- popup.showAsDropDown(anchor, xoff, yoff, gravity);
- }
- }
-
- @RequiresApi(21)
- static class PopupWindowCompatApi21Impl extends PopupWindowCompatApi19Impl {
- private static final String TAG = "PopupWindowCompatApi21";
-
- private static Field sOverlapAnchorField;
-
- static {
- try {
- sOverlapAnchorField = PopupWindow.class.getDeclaredField("mOverlapAnchor");
- sOverlapAnchorField.setAccessible(true);
- } catch (NoSuchFieldException e) {
- Log.i(TAG, "Could not fetch mOverlapAnchor field from PopupWindow", e);
- }
- }
-
- @Override
- public void setOverlapAnchor(PopupWindow popupWindow, boolean overlapAnchor) {
- if (sOverlapAnchorField != null) {
- try {
- sOverlapAnchorField.set(popupWindow, overlapAnchor);
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Could not set overlap anchor field in PopupWindow", e);
- }
- }
- }
-
- @Override
- public boolean getOverlapAnchor(PopupWindow popupWindow) {
- if (sOverlapAnchorField != null) {
- try {
- return (Boolean) sOverlapAnchorField.get(popupWindow);
- } catch (IllegalAccessException e) {
- Log.i(TAG, "Could not get overlap anchor field in PopupWindow", e);
- }
- }
- return false;
- }
- }
-
- @RequiresApi(23)
- static class PopupWindowCompatApi23Impl extends PopupWindowCompatApi21Impl {
- @Override
- public void setOverlapAnchor(PopupWindow popupWindow, boolean overlapAnchor) {
- popupWindow.setOverlapAnchor(overlapAnchor);
- }
-
- @Override
- public boolean getOverlapAnchor(PopupWindow popupWindow) {
- return popupWindow.getOverlapAnchor();
- }
-
- @Override
- public void setWindowLayoutType(PopupWindow popupWindow, int layoutType) {
- popupWindow.setWindowLayoutType(layoutType);
- }
-
- @Override
- public int getWindowLayoutType(PopupWindow popupWindow) {
- return popupWindow.getWindowLayoutType();
- }
- }
-
- /**
- * Select the correct implementation to use for the current platform.
- */
- static final PopupWindowCompatBaseImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 23) {
- IMPL = new PopupWindowCompatApi23Impl();
- } else if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new PopupWindowCompatApi21Impl();
- } else if (Build.VERSION.SDK_INT >= 19) {
- IMPL = new PopupWindowCompatApi19Impl();
- } else {
- IMPL = new PopupWindowCompatBaseImpl();
- }
- }
+ private static Field sOverlapAnchorField;
+ private static boolean sOverlapAnchorFieldAttempted;
private PopupWindowCompat() {
// This class is not publicly instantiable.
@@ -217,7 +65,19 @@
*/
public static void showAsDropDown(@NonNull PopupWindow popup, @NonNull View anchor,
int xoff, int yoff, int gravity) {
- IMPL.showAsDropDown(popup, anchor, xoff, yoff, gravity);
+ if (Build.VERSION.SDK_INT >= 19) {
+ popup.showAsDropDown(anchor, xoff, yoff, gravity);
+ } else {
+ int xoff1 = xoff;
+ final int hgrav = GravityCompat.getAbsoluteGravity(gravity,
+ ViewCompat.getLayoutDirection(anchor)) & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if (hgrav == Gravity.RIGHT) {
+ // Flip the location to align the right sides of the popup and
+ // anchor instead of left.
+ xoff1 -= (popup.getWidth() - anchor.getWidth());
+ }
+ popup.showAsDropDown(anchor, xoff1, yoff);
+ }
}
/**
@@ -227,7 +87,26 @@
* @param overlapAnchor Whether the popup should overlap its anchor.
*/
public static void setOverlapAnchor(@NonNull PopupWindow popupWindow, boolean overlapAnchor) {
- IMPL.setOverlapAnchor(popupWindow, overlapAnchor);
+ if (Build.VERSION.SDK_INT >= 23) {
+ popupWindow.setOverlapAnchor(overlapAnchor);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ if (!sOverlapAnchorFieldAttempted) {
+ try {
+ sOverlapAnchorField = PopupWindow.class.getDeclaredField("mOverlapAnchor");
+ sOverlapAnchorField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "Could not fetch mOverlapAnchor field from PopupWindow", e);
+ }
+ sOverlapAnchorFieldAttempted = true;
+ }
+ if (sOverlapAnchorField != null) {
+ try {
+ sOverlapAnchorField.set(popupWindow, overlapAnchor);
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Could not set overlap anchor field in PopupWindow", e);
+ }
+ }
+ }
}
/**
@@ -237,7 +116,28 @@
* @return Whether the popup should overlap its anchor.
*/
public static boolean getOverlapAnchor(@NonNull PopupWindow popupWindow) {
- return IMPL.getOverlapAnchor(popupWindow);
+ if (Build.VERSION.SDK_INT >= 23) {
+ return popupWindow.getOverlapAnchor();
+ }
+ if (Build.VERSION.SDK_INT >= 21) {
+ if (!sOverlapAnchorFieldAttempted) {
+ try {
+ sOverlapAnchorField = PopupWindow.class.getDeclaredField("mOverlapAnchor");
+ sOverlapAnchorField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "Could not fetch mOverlapAnchor field from PopupWindow", e);
+ }
+ sOverlapAnchorFieldAttempted = true;
+ }
+ if (sOverlapAnchorField != null) {
+ try {
+ return (Boolean) sOverlapAnchorField.get(popupWindow);
+ } catch (IllegalAccessException e) {
+ Log.i(TAG, "Could not get overlap anchor field in PopupWindow", e);
+ }
+ }
+ }
+ return false;
}
/**
@@ -250,7 +150,28 @@
* @see android.view.WindowManager.LayoutParams#type
*/
public static void setWindowLayoutType(@NonNull PopupWindow popupWindow, int layoutType) {
- IMPL.setWindowLayoutType(popupWindow, layoutType);
+ if (Build.VERSION.SDK_INT >= 23) {
+ popupWindow.setWindowLayoutType(layoutType);
+ return;
+ }
+
+ if (!sSetWindowLayoutTypeMethodAttempted) {
+ try {
+ sSetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
+ "setWindowLayoutType", int.class);
+ sSetWindowLayoutTypeMethod.setAccessible(true);
+ } catch (Exception e) {
+ // Reflection method fetch failed. Oh well.
+ }
+ sSetWindowLayoutTypeMethodAttempted = true;
+ }
+ if (sSetWindowLayoutTypeMethod != null) {
+ try {
+ sSetWindowLayoutTypeMethod.invoke(popupWindow, layoutType);
+ } catch (Exception e) {
+ // Reflection call failed. Oh well.
+ }
+ }
}
/**
@@ -259,6 +180,27 @@
* @see #setWindowLayoutType(PopupWindow popupWindow, int)
*/
public static int getWindowLayoutType(@NonNull PopupWindow popupWindow) {
- return IMPL.getWindowLayoutType(popupWindow);
+ if (Build.VERSION.SDK_INT >= 23) {
+ return popupWindow.getWindowLayoutType();
+ }
+
+ if (!sGetWindowLayoutTypeMethodAttempted) {
+ try {
+ sGetWindowLayoutTypeMethod = PopupWindow.class.getDeclaredMethod(
+ "getWindowLayoutType");
+ sGetWindowLayoutTypeMethod.setAccessible(true);
+ } catch (Exception e) {
+ // Reflection method fetch failed. Oh well.
+ }
+ sGetWindowLayoutTypeMethodAttempted = true;
+ }
+ if (sGetWindowLayoutTypeMethod != null) {
+ try {
+ return (Integer) sGetWindowLayoutTypeMethod.invoke(popupWindow);
+ } catch (Exception e) {
+ // Reflection call failed. Oh well.
+ }
+ }
+ return 0;
}
}
diff --git a/compat/src/main/java/androidx/core/widget/TextViewCompat.java b/compat/src/main/java/androidx/core/widget/TextViewCompat.java
index f82e025..d67d61c 100644
--- a/compat/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/compat/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -38,7 +38,6 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
@@ -54,6 +53,7 @@
* Helper for accessing features in {@link TextView}.
*/
public final class TextViewCompat {
+ private static final String LOG_TAG = "TextViewCompat";
/**
* The TextView does not auto-size text (default).
@@ -72,486 +72,39 @@
@Retention(RetentionPolicy.SOURCE)
public @interface AutoSizeTextType {}
+ private static Field sMaximumField;
+ private static boolean sMaximumFieldFetched;
+ private static Field sMaxModeField;
+ private static boolean sMaxModeFieldFetched;
+
+ private static Field sMinimumField;
+ private static boolean sMinimumFieldFetched;
+ private static Field sMinModeField;
+ private static boolean sMinModeFieldFetched;
+
+ private static final int LINES = 1;
+
// Hide constructor
private TextViewCompat() {}
- static class TextViewCompatBaseImpl {
- private static final String LOG_TAG = "TextViewCompatBase";
- private static final int LINES = 1;
-
- private static Field sMaximumField;
- private static boolean sMaximumFieldFetched;
- private static Field sMaxModeField;
- private static boolean sMaxModeFieldFetched;
-
- private static Field sMinimumField;
- private static boolean sMinimumFieldFetched;
- private static Field sMinModeField;
- private static boolean sMinModeFieldFetched;
-
- public void setCompoundDrawablesRelative(@NonNull TextView textView,
- @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
- @Nullable Drawable bottom) {
- textView.setCompoundDrawables(start, top, end, bottom);
+ private static Field retrieveField(String fieldName) {
+ Field field = null;
+ try {
+ field = TextView.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.e(LOG_TAG, "Could not retrieve " + fieldName + " field.");
}
-
- public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
- @Nullable Drawable bottom) {
- textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
- }
-
- public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
- @DrawableRes int bottom) {
- textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
- }
-
- private static Field retrieveField(String fieldName) {
- Field field = null;
- try {
- field = TextView.class.getDeclaredField(fieldName);
- field.setAccessible(true);
- } catch (NoSuchFieldException e) {
- Log.e(LOG_TAG, "Could not retrieve " + fieldName + " field.");
- }
- return field;
- }
-
- private static int retrieveIntFromField(Field field, TextView textView) {
- try {
- return field.getInt(textView);
- } catch (IllegalAccessException e) {
- Log.d(LOG_TAG, "Could not retrieve value of " + field.getName() + " field.");
- }
- return -1;
- }
-
- public int getMaxLines(TextView textView) {
- if (!sMaxModeFieldFetched) {
- sMaxModeField = retrieveField("mMaxMode");
- sMaxModeFieldFetched = true;
- }
- if (sMaxModeField != null && retrieveIntFromField(sMaxModeField, textView) == LINES) {
- // If the max mode is using lines, we can grab the maximum value
- if (!sMaximumFieldFetched) {
- sMaximumField = retrieveField("mMaximum");
- sMaximumFieldFetched = true;
- }
- if (sMaximumField != null) {
- return retrieveIntFromField(sMaximumField, textView);
- }
- }
- return -1;
- }
-
- public int getMinLines(TextView textView) {
- if (!sMinModeFieldFetched) {
- sMinModeField = retrieveField("mMinMode");
- sMinModeFieldFetched = true;
- }
- if (sMinModeField != null && retrieveIntFromField(sMinModeField, textView) == LINES) {
- // If the min mode is using lines, we can grab the maximum value
- if (!sMinimumFieldFetched) {
- sMinimumField = retrieveField("mMinimum");
- sMinimumFieldFetched = true;
- }
- if (sMinimumField != null) {
- return retrieveIntFromField(sMinimumField, textView);
- }
- }
- return -1;
- }
-
- @SuppressWarnings("deprecation")
- public void setTextAppearance(TextView textView, @StyleRes int resId) {
- textView.setTextAppearance(textView.getContext(), resId);
- }
-
- public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
- return textView.getCompoundDrawables();
- }
-
- public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
- if (textView instanceof AutoSizeableTextView) {
- ((AutoSizeableTextView) textView).setAutoSizeTextTypeWithDefaults(autoSizeTextType);
- }
- }
-
- public void setAutoSizeTextTypeUniformWithConfiguration(
- TextView textView,
- int autoSizeMinTextSize,
- int autoSizeMaxTextSize,
- int autoSizeStepGranularity,
- int unit) throws IllegalArgumentException {
- if (textView instanceof AutoSizeableTextView) {
- ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration(
- autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
- }
- }
-
- public void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView,
- @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
- if (textView instanceof AutoSizeableTextView) {
- ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes(
- presetSizes, unit);
- }
- }
-
- public int getAutoSizeTextType(TextView textView) {
- if (textView instanceof AutoSizeableTextView) {
- return ((AutoSizeableTextView) textView).getAutoSizeTextType();
- }
- return AUTO_SIZE_TEXT_TYPE_NONE;
- }
-
- public int getAutoSizeStepGranularity(TextView textView) {
- if (textView instanceof AutoSizeableTextView) {
- return ((AutoSizeableTextView) textView).getAutoSizeStepGranularity();
- }
- return -1;
- }
-
- public int getAutoSizeMinTextSize(TextView textView) {
- if (textView instanceof AutoSizeableTextView) {
- return ((AutoSizeableTextView) textView).getAutoSizeMinTextSize();
- }
- return -1;
- }
-
- public int getAutoSizeMaxTextSize(TextView textView) {
- if (textView instanceof AutoSizeableTextView) {
- return ((AutoSizeableTextView) textView).getAutoSizeMaxTextSize();
- }
- return -1;
- }
-
- public int[] getAutoSizeTextAvailableSizes(TextView textView) {
- if (textView instanceof AutoSizeableTextView) {
- return ((AutoSizeableTextView) textView).getAutoSizeTextAvailableSizes();
- }
- return new int[0];
- }
-
- public void setCustomSelectionActionModeCallback(TextView textView,
- ActionMode.Callback callback) {
- textView.setCustomSelectionActionModeCallback(callback);
- }
+ return field;
}
- @RequiresApi(16)
- static class TextViewCompatApi16Impl extends TextViewCompatBaseImpl {
- @Override
- public int getMaxLines(TextView textView) {
- return textView.getMaxLines();
+ private static int retrieveIntFromField(Field field, TextView textView) {
+ try {
+ return field.getInt(textView);
+ } catch (IllegalAccessException e) {
+ Log.d(LOG_TAG, "Could not retrieve value of " + field.getName() + " field.");
}
-
- @Override
- public int getMinLines(TextView textView) {
- return textView.getMinLines();
- }
- }
-
- @RequiresApi(17)
- static class TextViewCompatApi17Impl extends TextViewCompatApi16Impl {
- @Override
- public void setCompoundDrawablesRelative(@NonNull TextView textView,
- @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
- @Nullable Drawable bottom) {
- boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
- }
-
- @Override
- public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
- @Nullable Drawable bottom) {
- boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
- rtl ? start : end, bottom);
- }
-
- @Override
- public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
- @DrawableRes int bottom) {
- boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
- rtl ? start : end, bottom);
- }
-
- @Override
- public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
- final boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- final Drawable[] compounds = textView.getCompoundDrawables();
- if (rtl) {
- // If we're on RTL, we need to invert the horizontal result like above
- final Drawable start = compounds[2];
- final Drawable end = compounds[0];
- compounds[0] = start;
- compounds[2] = end;
- }
- return compounds;
- }
- }
-
- @RequiresApi(18)
- static class TextViewCompatApi18Impl extends TextViewCompatApi17Impl {
- @Override
- public void setCompoundDrawablesRelative(@NonNull TextView textView,
- @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
- @Nullable Drawable bottom) {
- textView.setCompoundDrawablesRelative(start, top, end, bottom);
- }
-
- @Override
- public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
- @Nullable Drawable bottom) {
- textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
- }
-
- @Override
- public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
- @DrawableRes int bottom) {
- textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
- }
-
- @Override
- public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
- return textView.getCompoundDrawablesRelative();
- }
- }
-
- @RequiresApi(23)
- static class TextViewCompatApi23Impl extends TextViewCompatApi18Impl {
- @Override
- public void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
- textView.setTextAppearance(resId);
- }
- }
-
- @RequiresApi(26)
- static class TextViewCompatApi26Impl extends TextViewCompatApi23Impl {
- @Override
- public void setCustomSelectionActionModeCallback(final TextView textView,
- final ActionMode.Callback callback) {
- if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O
- && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
- super.setCustomSelectionActionModeCallback(textView, callback);
- return;
- }
-
-
- // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
- // intent after selection to not be displayed in the menu, although they should be.
- // Here we fix this, by removing the menu items created by the framework code, and
- // adding them (and the missing ones) back correctly.
- textView.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
- // This constant should be correlated with its definition in the
- // android.widget.Editor class.
- private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
-
- // References to the MenuBuilder class and its removeItemAt(int) method.
- // Since in most cases the menu instance processed by this callback is going
- // to be a MenuBuilder, we keep these references to avoid querying for them
- // frequently by reflection in recomputeProcessTextMenuItems.
- private Class mMenuBuilderClass;
- private Method mMenuBuilderRemoveItemAtMethod;
- private boolean mCanUseMenuBuilderReferences;
- private boolean mInitializedMenuBuilderReferences = false;
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- return callback.onCreateActionMode(mode, menu);
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- recomputeProcessTextMenuItems(menu);
- return callback.onPrepareActionMode(mode, menu);
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- return callback.onActionItemClicked(mode, item);
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- callback.onDestroyActionMode(mode);
- }
-
- private void recomputeProcessTextMenuItems(final Menu menu) {
- final Context context = textView.getContext();
- final PackageManager packageManager = context.getPackageManager();
-
- if (!mInitializedMenuBuilderReferences) {
- mInitializedMenuBuilderReferences = true;
- try {
- mMenuBuilderClass =
- Class.forName("com.android.internal.view.menu.MenuBuilder");
- mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
- .getDeclaredMethod("removeItemAt", Integer.TYPE);
- mCanUseMenuBuilderReferences = true;
- } catch (ClassNotFoundException | NoSuchMethodException e) {
- mMenuBuilderClass = null;
- mMenuBuilderRemoveItemAtMethod = null;
- mCanUseMenuBuilderReferences = false;
- }
- }
- // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
- try {
- final Method removeItemAtMethod =
- (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
- ? mMenuBuilderRemoveItemAtMethod
- : menu.getClass()
- .getDeclaredMethod("removeItemAt", Integer.TYPE);
- for (int i = menu.size() - 1; i >= 0; --i) {
- final MenuItem item = menu.getItem(i);
- if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
- .equals(item.getIntent().getAction())) {
- removeItemAtMethod.invoke(menu, i);
- }
- }
- } catch (NoSuchMethodException | IllegalAccessException
- | InvocationTargetException e) {
- // There is a menu custom implementation used which is not providing
- // a removeItemAt(int) menu. There is nothing we can do in this case.
- return;
- }
-
- // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
- final List<ResolveInfo> supportedActivities =
- getSupportedActivities(context, packageManager);
- for (int i = 0; i < supportedActivities.size(); ++i) {
- final ResolveInfo info = supportedActivities.get(i);
- menu.add(Menu.NONE, Menu.NONE,
- MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
- info.loadLabel(packageManager))
- .setIntent(createProcessTextIntentForResolveInfo(info, textView))
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- }
- }
-
- private List<ResolveInfo> getSupportedActivities(final Context context,
- final PackageManager packageManager) {
- final List<ResolveInfo> supportedActivities = new ArrayList<>();
- boolean canStartActivityForResult = context instanceof Activity;
- if (!canStartActivityForResult) {
- return supportedActivities;
- }
- final List<ResolveInfo> unfiltered =
- packageManager.queryIntentActivities(createProcessTextIntent(), 0);
- for (ResolveInfo info : unfiltered) {
- if (isSupportedActivity(info, context)) {
- supportedActivities.add(info);
- }
- }
- return supportedActivities;
- }
-
- private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
- if (context.getPackageName().equals(info.activityInfo.packageName)) {
- return true;
- }
- if (!info.activityInfo.exported) {
- return false;
- }
- return info.activityInfo.permission == null
- || context.checkSelfPermission(info.activityInfo.permission)
- == PackageManager.PERMISSION_GRANTED;
- }
-
- private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
- final TextView textView) {
- return createProcessTextIntent()
- .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView))
- .setClassName(info.activityInfo.packageName, info.activityInfo.name);
- }
-
- private boolean isEditable(final TextView textView) {
- return textView instanceof Editable
- && textView.onCheckIsTextEditor()
- && textView.isEnabled();
- }
-
- private Intent createProcessTextIntent() {
- return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
- }
- });
- }
- }
-
- @RequiresApi(27)
- static class TextViewCompatApi27Impl extends TextViewCompatApi26Impl {
- @Override
- public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
- textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
- }
-
- @Override
- public void setAutoSizeTextTypeUniformWithConfiguration(
- TextView textView,
- int autoSizeMinTextSize,
- int autoSizeMaxTextSize,
- int autoSizeStepGranularity,
- int unit) throws IllegalArgumentException {
- textView.setAutoSizeTextTypeUniformWithConfiguration(
- autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
- }
-
- @Override
- public void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView,
- @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
- textView.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
- }
-
- @Override
- public int getAutoSizeTextType(TextView textView) {
- return textView.getAutoSizeTextType();
- }
-
- @Override
- public int getAutoSizeStepGranularity(TextView textView) {
- return textView.getAutoSizeStepGranularity();
- }
-
- @Override
- public int getAutoSizeMinTextSize(TextView textView) {
- return textView.getAutoSizeMinTextSize();
- }
-
- @Override
- public int getAutoSizeMaxTextSize(TextView textView) {
- return textView.getAutoSizeMaxTextSize();
- }
-
- @Override
- public int[] getAutoSizeTextAvailableSizes(TextView textView) {
- return textView.getAutoSizeTextAvailableSizes();
- }
- }
-
- static final TextViewCompatBaseImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 27) {
- IMPL = new TextViewCompatApi27Impl();
- } else if (Build.VERSION.SDK_INT >= 26) {
- IMPL = new TextViewCompatApi26Impl();
- } else if (Build.VERSION.SDK_INT >= 23) {
- IMPL = new TextViewCompatApi23Impl();
- } else if (Build.VERSION.SDK_INT >= 18) {
- IMPL = new TextViewCompatApi18Impl();
- } else if (Build.VERSION.SDK_INT >= 17) {
- IMPL = new TextViewCompatApi17Impl();
- } else if (Build.VERSION.SDK_INT >= 16) {
- IMPL = new TextViewCompatApi16Impl();
- } else {
- IMPL = new TextViewCompatBaseImpl();
- }
+ return -1;
}
/**
@@ -572,7 +125,14 @@
public static void setCompoundDrawablesRelative(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
- IMPL.setCompoundDrawablesRelative(textView, start, top, end, bottom);
+ if (Build.VERSION.SDK_INT >= 18) {
+ textView.setCompoundDrawablesRelative(start, top, end, bottom);
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
+ } else {
+ textView.setCompoundDrawables(start, top, end, bottom);
+ }
}
/**
@@ -592,7 +152,15 @@
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
- IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom);
+ if (Build.VERSION.SDK_INT >= 18) {
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
+ rtl ? start : end, bottom);
+ } else {
+ textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
+ }
}
/**
@@ -616,7 +184,15 @@
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
@DrawableRes int bottom) {
- IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom);
+ if (Build.VERSION.SDK_INT >= 18) {
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
+ rtl ? start : end, bottom);
+ } else {
+ textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
+ }
}
/**
@@ -624,7 +200,25 @@
* height was set in pixels instead.
*/
public static int getMaxLines(@NonNull TextView textView) {
- return IMPL.getMaxLines(textView);
+ if (Build.VERSION.SDK_INT >= 16) {
+ return textView.getMaxLines();
+ }
+
+ if (!sMaxModeFieldFetched) {
+ sMaxModeField = retrieveField("mMaxMode");
+ sMaxModeFieldFetched = true;
+ }
+ if (sMaxModeField != null && retrieveIntFromField(sMaxModeField, textView) == LINES) {
+ // If the max mode is using lines, we can grab the maximum value
+ if (!sMaximumFieldFetched) {
+ sMaximumField = retrieveField("mMaximum");
+ sMaximumFieldFetched = true;
+ }
+ if (sMaximumField != null) {
+ return retrieveIntFromField(sMaximumField, textView);
+ }
+ }
+ return -1;
}
/**
@@ -632,7 +226,25 @@
* height was set in pixels instead.
*/
public static int getMinLines(@NonNull TextView textView) {
- return IMPL.getMinLines(textView);
+ if (Build.VERSION.SDK_INT >= 16) {
+ return textView.getMinLines();
+ }
+
+ if (!sMinModeFieldFetched) {
+ sMinModeField = retrieveField("mMinMode");
+ sMinModeFieldFetched = true;
+ }
+ if (sMinModeField != null && retrieveIntFromField(sMinModeField, textView) == LINES) {
+ // If the min mode is using lines, we can grab the maximum value
+ if (!sMinimumFieldFetched) {
+ sMinimumField = retrieveField("mMinimum");
+ sMinimumFieldFetched = true;
+ }
+ if (sMinimumField != null) {
+ return retrieveIntFromField(sMinimumField, textView);
+ }
+ }
+ return -1;
}
/**
@@ -645,7 +257,11 @@
* @param resId The resource identifier of the style to apply.
*/
public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
- IMPL.setTextAppearance(textView, resId);
+ if (Build.VERSION.SDK_INT >= 23) {
+ textView.setTextAppearance(resId);
+ } else {
+ textView.setTextAppearance(textView.getContext(), resId);
+ }
}
/**
@@ -653,7 +269,22 @@
*/
@NonNull
public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
- return IMPL.getCompoundDrawablesRelative(textView);
+ if (Build.VERSION.SDK_INT >= 18) {
+ return textView.getCompoundDrawablesRelative();
+ }
+ if (Build.VERSION.SDK_INT >= 17) {
+ final boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ final Drawable[] compounds = textView.getCompoundDrawables();
+ if (rtl) {
+ // If we're on RTL, we need to invert the horizontal result like above
+ final Drawable start = compounds[2];
+ final Drawable end = compounds[0];
+ compounds[0] = start;
+ compounds[2] = end;
+ }
+ return compounds;
+ }
+ return textView.getCompoundDrawables();
}
/**
@@ -666,9 +297,14 @@
*
* @attr name android:autoSizeTextType
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void setAutoSizeTextTypeWithDefaults(@NonNull TextView textView,
int autoSizeTextType) {
- IMPL.setAutoSizeTextTypeWithDefaults(textView, autoSizeTextType);
+ if (Build.VERSION.SDK_INT >= 27) {
+ textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
+ } else if (textView instanceof AutoSizeableTextView) {
+ ((AutoSizeableTextView) textView).setAutoSizeTextTypeWithDefaults(autoSizeTextType);
+ }
}
/**
@@ -692,14 +328,20 @@
* @attr name android:autoSizeMaxTextSize
* @attr name android:autoSizeStepGranularity
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void setAutoSizeTextTypeUniformWithConfiguration(
@NonNull TextView textView,
int autoSizeMinTextSize,
int autoSizeMaxTextSize,
int autoSizeStepGranularity,
int unit) throws IllegalArgumentException {
- IMPL.setAutoSizeTextTypeUniformWithConfiguration(textView, autoSizeMinTextSize,
- autoSizeMaxTextSize, autoSizeStepGranularity, unit);
+ if (Build.VERSION.SDK_INT >= 27) {
+ textView.setAutoSizeTextTypeUniformWithConfiguration(
+ autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
+ } else if (textView instanceof AutoSizeableTextView) {
+ ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration(
+ autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
+ }
}
/**
@@ -716,9 +358,15 @@
* @attr name android:autoSizeTextType
* @attr name android:autoSizePresetSizes
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull TextView textView,
@NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
- IMPL.setAutoSizeTextTypeUniformWithPresetSizes(textView, presetSizes, unit);
+ if (Build.VERSION.SDK_INT >= 27) {
+ textView.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
+ } else if (textView instanceof AutoSizeableTextView) {
+ ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes(
+ presetSizes, unit);
+ }
}
/**
@@ -730,8 +378,15 @@
*
* @attr name android:autoSizeTextType
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static int getAutoSizeTextType(@NonNull TextView textView) {
- return IMPL.getAutoSizeTextType(textView);
+ if (Build.VERSION.SDK_INT >= 27) {
+ return textView.getAutoSizeTextType();
+ }
+ if (textView instanceof AutoSizeableTextView) {
+ return ((AutoSizeableTextView) textView).getAutoSizeTextType();
+ }
+ return AUTO_SIZE_TEXT_TYPE_NONE;
}
/**
@@ -739,8 +394,15 @@
*
* @attr name android:autoSizeStepGranularity
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static int getAutoSizeStepGranularity(@NonNull TextView textView) {
- return IMPL.getAutoSizeStepGranularity(textView);
+ if (Build.VERSION.SDK_INT >= 27) {
+ return textView.getAutoSizeStepGranularity();
+ }
+ if (textView instanceof AutoSizeableTextView) {
+ return ((AutoSizeableTextView) textView).getAutoSizeStepGranularity();
+ }
+ return -1;
}
/**
@@ -749,8 +411,15 @@
*
* @attr name android:autoSizeMinTextSize
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static int getAutoSizeMinTextSize(@NonNull TextView textView) {
- return IMPL.getAutoSizeMinTextSize(textView);
+ if (Build.VERSION.SDK_INT >= 27) {
+ return textView.getAutoSizeMinTextSize();
+ }
+ if (textView instanceof AutoSizeableTextView) {
+ return ((AutoSizeableTextView) textView).getAutoSizeMinTextSize();
+ }
+ return -1;
}
/**
@@ -759,8 +428,15 @@
*
* @attr name android:autoSizeMaxTextSize
*/
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static int getAutoSizeMaxTextSize(@NonNull TextView textView) {
- return IMPL.getAutoSizeMaxTextSize(textView);
+ if (Build.VERSION.SDK_INT >= 27) {
+ return textView.getAutoSizeMaxTextSize();
+ }
+ if (textView instanceof AutoSizeableTextView) {
+ return ((AutoSizeableTextView) textView).getAutoSizeMaxTextSize();
+ }
+ return -1;
}
/**
@@ -769,8 +445,15 @@
* @attr name android:autoSizePresetSizes
*/
@NonNull
+ @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
- return IMPL.getAutoSizeTextAvailableSizes(textView);
+ if (Build.VERSION.SDK_INT >= 27) {
+ return textView.getAutoSizeTextAvailableSizes();
+ }
+ if (textView instanceof AutoSizeableTextView) {
+ return ((AutoSizeableTextView) textView).getAutoSizeTextAvailableSizes();
+ }
+ return new int[0];
}
/**
@@ -795,8 +478,149 @@
* @param textView The TextView to set the action selection mode callback on.
* @param callback The action selection mode callback to set on textView.
*/
- public static void setCustomSelectionActionModeCallback(@NonNull TextView textView,
- @NonNull ActionMode.Callback callback) {
- IMPL.setCustomSelectionActionModeCallback(textView, callback);
+ public static void setCustomSelectionActionModeCallback(@NonNull final TextView textView,
+ @NonNull final ActionMode.Callback callback) {
+ if (Build.VERSION.SDK_INT < 26 || Build.VERSION.SDK_INT > 27) {
+ textView.setCustomSelectionActionModeCallback(callback);
+ return;
+ }
+
+ // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
+ // intent after selection to not be displayed in the menu, although they should be.
+ // Here we fix this, by removing the menu items created by the framework code, and
+ // adding them (and the missing ones) back correctly.
+ textView.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
+ // This constant should be correlated with its definition in the
+ // android.widget.Editor class.
+ private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+ // References to the MenuBuilder class and its removeItemAt(int) method.
+ // Since in most cases the menu instance processed by this callback is going
+ // to be a MenuBuilder, we keep these references to avoid querying for them
+ // frequently by reflection in recomputeProcessTextMenuItems.
+ private Class mMenuBuilderClass;
+ private Method mMenuBuilderRemoveItemAtMethod;
+ private boolean mCanUseMenuBuilderReferences;
+ private boolean mInitializedMenuBuilderReferences = false;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return callback.onCreateActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ recomputeProcessTextMenuItems(menu);
+ return callback.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return callback.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ callback.onDestroyActionMode(mode);
+ }
+
+ private void recomputeProcessTextMenuItems(final Menu menu) {
+ final Context context = textView.getContext();
+ final PackageManager packageManager = context.getPackageManager();
+
+ if (!mInitializedMenuBuilderReferences) {
+ mInitializedMenuBuilderReferences = true;
+ try {
+ mMenuBuilderClass =
+ Class.forName("com.android.internal.view.menu.MenuBuilder");
+ mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ mCanUseMenuBuilderReferences = true;
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ mMenuBuilderClass = null;
+ mMenuBuilderRemoveItemAtMethod = null;
+ mCanUseMenuBuilderReferences = false;
+ }
+ }
+ // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
+ try {
+ final Method removeItemAtMethod =
+ (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
+ ? mMenuBuilderRemoveItemAtMethod
+ : menu.getClass()
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ for (int i = menu.size() - 1; i >= 0; --i) {
+ final MenuItem item = menu.getItem(i);
+ if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
+ .equals(item.getIntent().getAction())) {
+ removeItemAtMethod.invoke(menu, i);
+ }
+ }
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ // There is a menu custom implementation used which is not providing
+ // a removeItemAt(int) menu. There is nothing we can do in this case.
+ return;
+ }
+
+ // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
+ final List<ResolveInfo> supportedActivities =
+ getSupportedActivities(context, packageManager);
+ for (int i = 0; i < supportedActivities.size(); ++i) {
+ final ResolveInfo info = supportedActivities.get(i);
+ menu.add(Menu.NONE, Menu.NONE,
+ MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
+ info.loadLabel(packageManager))
+ .setIntent(createProcessTextIntentForResolveInfo(info, textView))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ }
+
+ private List<ResolveInfo> getSupportedActivities(final Context context,
+ final PackageManager packageManager) {
+ final List<ResolveInfo> supportedActivities = new ArrayList<>();
+ boolean canStartActivityForResult = context instanceof Activity;
+ if (!canStartActivityForResult) {
+ return supportedActivities;
+ }
+ final List<ResolveInfo> unfiltered =
+ packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+ for (ResolveInfo info : unfiltered) {
+ if (isSupportedActivity(info, context)) {
+ supportedActivities.add(info);
+ }
+ }
+ return supportedActivities;
+ }
+
+ private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
+ if (context.getPackageName().equals(info.activityInfo.packageName)) {
+ return true;
+ }
+ if (!info.activityInfo.exported) {
+ return false;
+ }
+ return info.activityInfo.permission == null
+ || context.checkSelfPermission(info.activityInfo.permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
+ final TextView textView11) {
+ return createProcessTextIntent()
+ .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView11))
+ .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+ }
+
+ private boolean isEditable(final TextView textView11) {
+ return textView11 instanceof Editable
+ && textView11.onCheckIsTextEditor()
+ && textView11.isEnabled();
+ }
+
+ private Intent createProcessTextIntent() {
+ return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
+ }
+ });
}
}
diff --git a/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java b/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
index f32b67a..6776257 100644
--- a/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
+++ b/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
@@ -140,7 +140,7 @@
*/
public static boolean isDocumentUri(Context context, Uri uri) {
if (Build.VERSION.SDK_INT >= 19) {
- return DocumentsContractApi19.isDocumentUri(context, uri);
+ return DocumentsContract.isDocumentUri(context, uri);
} else {
return false;
}
diff --git a/documentfile/src/main/java/androidx/documentfile/provider/DocumentsContractApi19.java b/documentfile/src/main/java/androidx/documentfile/provider/DocumentsContractApi19.java
index aefbe70..750958d 100644
--- a/documentfile/src/main/java/androidx/documentfile/provider/DocumentsContractApi19.java
+++ b/documentfile/src/main/java/androidx/documentfile/provider/DocumentsContractApi19.java
@@ -35,12 +35,8 @@
// DocumentsContract API level 24.
private static final int FLAG_VIRTUAL_DOCUMENT = 1 << 9;
- public static boolean isDocumentUri(Context context, Uri self) {
- return DocumentsContract.isDocumentUri(context, self);
- }
-
public static boolean isVirtual(Context context, Uri self) {
- if (!isDocumentUri(context, self)) {
+ if (!DocumentsContract.isDocumentUri(context, self)) {
return false;
}
diff --git a/fragment/api/current.txt b/fragment/api/current.txt
index 95ed05f..1ee47e3 100644
--- a/fragment/api/current.txt
+++ b/fragment/api/current.txt
@@ -23,7 +23,7 @@
field public static final int STYLE_NO_TITLE = 1; // 0x1
}
- public class Fragment implements android.content.ComponentCallbacks android.arch.lifecycle.LifecycleOwner android.view.View.OnCreateContextMenuListener android.arch.lifecycle.ViewModelStoreOwner {
+ public class Fragment implements android.content.ComponentCallbacks androidx.lifecycle.LifecycleOwner android.view.View.OnCreateContextMenuListener androidx.lifecycle.ViewModelStoreOwner {
ctor public Fragment();
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
method public final boolean equals(java.lang.Object);
@@ -39,7 +39,7 @@
method public final java.lang.Object getHost();
method public final int getId();
method public final android.view.LayoutInflater getLayoutInflater();
- method public android.arch.lifecycle.Lifecycle getLifecycle();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
method public deprecated androidx.loader.app.LoaderManager getLoaderManager();
method public final androidx.fragment.app.Fragment getParentFragment();
method public java.lang.Object getReenterTransition();
@@ -56,7 +56,7 @@
method public final java.lang.CharSequence getText(int);
method public boolean getUserVisibleHint();
method public android.view.View getView();
- method public android.arch.lifecycle.ViewModelStore getViewModelStore();
+ method public androidx.lifecycle.ViewModelStore getViewModelStore();
method public final int hashCode();
method public static androidx.fragment.app.Fragment instantiate(android.content.Context, java.lang.String);
method public static androidx.fragment.app.Fragment instantiate(android.content.Context, java.lang.String, android.os.Bundle);
@@ -147,12 +147,12 @@
field public static final android.os.Parcelable.Creator<androidx.fragment.app.Fragment.SavedState> CREATOR;
}
- public class FragmentActivity extends androidx.core.app.SupportActivity implements androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback androidx.core.app.ActivityCompat.RequestPermissionsRequestCodeValidator android.arch.lifecycle.ViewModelStoreOwner {
+ public class FragmentActivity extends androidx.core.app.SupportActivity implements androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback androidx.core.app.ActivityCompat.RequestPermissionsRequestCodeValidator androidx.lifecycle.ViewModelStoreOwner {
ctor public FragmentActivity();
method public java.lang.Object getLastCustomNonConfigurationInstance();
method public androidx.fragment.app.FragmentManager getSupportFragmentManager();
method public deprecated androidx.loader.app.LoaderManager getSupportLoaderManager();
- method public android.arch.lifecycle.ViewModelStore getViewModelStore();
+ method public androidx.lifecycle.ViewModelStore getViewModelStore();
method public void onAttachFragment(androidx.fragment.app.Fragment);
method public void onMultiWindowModeChanged(boolean);
method public void onPictureInPictureModeChanged(boolean);
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.java
index b318e7b..9c3c7dc 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.java
@@ -18,9 +18,6 @@
import static org.junit.Assert.assertEquals;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
import android.os.Bundle;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
@@ -28,6 +25,9 @@
import android.support.test.runner.AndroidJUnit4;
import androidx.fragment.app.test.EmptyFragmentTestActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
import org.junit.Assert;
import org.junit.Rule;
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.java b/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.java
index 3f5cfd2..41cc738 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.java
@@ -16,8 +16,8 @@
package androidx.fragment.app;
-import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY;
-import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -25,9 +25,6 @@
import static org.hamcrest.MatcherAssert.assertThat;
import android.app.Instrumentation;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
-import android.arch.lifecycle.ViewModelProvider;
import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
@@ -37,6 +34,9 @@
import androidx.fragment.app.test.TestViewModel;
import androidx.fragment.app.test.ViewModelActivity;
import androidx.fragment.app.test.ViewModelActivity.ViewModelFragment;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.ViewModelProvider;
import org.junit.Rule;
import org.junit.Test;
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.java b/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.java
index 4e36716..df7f047 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.java
@@ -20,7 +20,6 @@
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.arch.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
@@ -30,6 +29,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.test.EmptyFragmentTestActivity;
import androidx.fragment.app.test.TestViewModel;
+import androidx.lifecycle.ViewModelProvider;
import org.junit.Rule;
import org.junit.Test;
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/TestViewModel.java b/fragment/src/androidTest/java/androidx/fragment/app/test/TestViewModel.java
index f509382..81433c7 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/TestViewModel.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/TestViewModel.java
@@ -16,7 +16,7 @@
package androidx.fragment.app.test;
-import android.arch.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModel;
public class TestViewModel extends ViewModel {
public boolean mCleared = false;
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/ViewModelActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/ViewModelActivity.java
index 3dca0cc..e13ed7d 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/ViewModelActivity.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/ViewModelActivity.java
@@ -16,13 +16,13 @@
package androidx.fragment.app.test;
-import android.arch.lifecycle.ViewModelProvider;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.test.R;
+import androidx.lifecycle.ViewModelProvider;
public class ViewModelActivity extends FragmentActivity {
public static final String KEY_FRAGMENT_MODEL = "fragment-model";
diff --git a/fragment/src/main/java/androidx/fragment/app/BaseFragmentActivityApi14.java b/fragment/src/main/java/androidx/fragment/app/BaseFragmentActivityApi14.java
deleted file mode 100644
index d069749..0000000
--- a/fragment/src/main/java/androidx/fragment/app/BaseFragmentActivityApi14.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2018 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 androidx.fragment.app;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.util.AttributeSet;
-import android.view.View;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.core.app.SupportActivity;
-
-@RequiresApi(14)
-abstract class BaseFragmentActivityApi14 extends SupportActivity {
-
- // We need to keep track of whether startIntentSenderForResult originated from a Fragment, so we
- // can conditionally check whether the requestCode collides with our reserved ID space for the
- // request index (see above). Unfortunately we can't just call
- // super.startIntentSenderForResult(...) to bypass the check when the call didn't come from a
- // fragment, since we need to use the ActivityCompat version for backward compatibility.
- boolean mStartedIntentSenderFromFragment;
-
- @Override
- public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
- final View v = dispatchFragmentsOnCreateView(parent, name, context, attrs);
- if (v == null) {
- return super.onCreateView(parent, name, context, attrs);
- }
- return v;
- }
-
- @Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
- final View v = dispatchFragmentsOnCreateView(null, name, context, attrs);
- if (v == null) {
- return super.onCreateView(name, context, attrs);
- }
- return v;
- }
-
- abstract View dispatchFragmentsOnCreateView(View parent, String name,
- Context context, AttributeSet attrs);
-
- @Override
- public void startIntentSenderForResult(IntentSender intent, int requestCode,
- @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)
- throws IntentSender.SendIntentException {
- // If this was started from a Fragment we've already checked the upper 16 bits were not in
- // use, and then repurposed them for the Fragment's index.
- if (!mStartedIntentSenderFromFragment) {
- if (requestCode != -1) {
- checkForValidRequestCode(requestCode);
- }
- }
- super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues,
- extraFlags);
- }
-
- /**
- * Checks whether the given request code is a valid code by masking it with 0xffff0000. Throws
- * an {@link IllegalArgumentException} if the code is not valid.
- */
- static void checkForValidRequestCode(int requestCode) {
- if ((requestCode & 0xffff0000) != 0) {
- throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
- }
- }
-}
diff --git a/fragment/src/main/java/androidx/fragment/app/BaseFragmentActivityApi16.java b/fragment/src/main/java/androidx/fragment/app/BaseFragmentActivityApi16.java
deleted file mode 100644
index 07703a4..0000000
--- a/fragment/src/main/java/androidx/fragment/app/BaseFragmentActivityApi16.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2018 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 androidx.fragment.app;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Intent;
-import android.content.IntentSender;
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-/**
- * Base class for {@code FragmentActivity} to be able to use v16 APIs.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-abstract class BaseFragmentActivityApi16 extends BaseFragmentActivityApi14 {
-
- // We need to keep track of whether startActivityForResult originated from a Fragment, so we
- // can conditionally check whether the requestCode collides with our reserved ID space for the
- // request index (see above). Unfortunately we can't just call
- // super.startActivityForResult(...) to bypass the check when the call didn't come from a
- // fragment, since we need to use the ActivityCompat version for backward compatibility.
- boolean mStartedActivityFromFragment;
-
- @RequiresApi(16)
- @Override
- public void startActivityForResult(Intent intent, int requestCode,
- @Nullable Bundle options) {
- // If this was started from a Fragment we've already checked the upper 16 bits were not in
- // use, and then repurposed them for the Fragment's index.
- if (!mStartedActivityFromFragment) {
- if (requestCode != -1) {
- checkForValidRequestCode(requestCode);
- }
- }
- super.startActivityForResult(intent, requestCode, options);
- }
-
- @RequiresApi(16)
- @Override
- public void startIntentSenderForResult(IntentSender intent, int requestCode,
- @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
- Bundle options) throws IntentSender.SendIntentException {
- // If this was started from a Fragment we've already checked the upper 16 bits were not in
- // use, and then repurposed them for the Fragment's index.
- if (!mStartedIntentSenderFromFragment) {
- if (requestCode != -1) {
- checkForValidRequestCode(requestCode);
- }
- }
- super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues,
- extraFlags, options);
- }
-}
diff --git a/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/src/main/java/androidx/fragment/app/Fragment.java
index cd186aa..90728c1 100644
--- a/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -20,11 +20,6 @@
import android.animation.Animator;
import android.app.Activity;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.LifecycleRegistry;
-import android.arch.lifecycle.ViewModelStore;
-import android.arch.lifecycle.ViewModelStoreOwner;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.Intent;
@@ -58,23 +53,17 @@
import androidx.core.app.SharedElementCallback;
import androidx.core.util.DebugUtils;
import androidx.core.view.LayoutInflaterCompat;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.ViewModelStore;
+import androidx.lifecycle.ViewModelStoreOwner;
import androidx.loader.app.LoaderManager;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.StringRes;
-import androidx.collection.SimpleArrayMap;
-import androidx.core.app.SharedElementCallback;
-import androidx.core.util.DebugUtils;
-import androidx.core.view.LayoutInflaterCompat;
-import androidx.loader.app.LoaderManager;
-
/**
* Static library support version of the framework's {@link android.app.Fragment}.
* Used to write apps that run on platforms prior to Android 3.0. When running
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
index e4abec4..1108775 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
@@ -19,10 +19,6 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.app.Activity;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.ViewModelStore;
-import android.arch.lifecycle.ViewModelStoreOwner;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
@@ -47,22 +43,17 @@
import androidx.collection.SparseArrayCompat;
import androidx.core.app.ActivityCompat;
import androidx.core.app.SharedElementCallback;
+import androidx.core.app.SupportActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.ViewModelStore;
+import androidx.lifecycle.ViewModelStoreOwner;
import androidx.loader.app.LoaderManager;
-import androidx.loader.content.Loader;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collection;
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.collection.SparseArrayCompat;
-import androidx.core.app.ActivityCompat;
-import androidx.core.app.SharedElementCallback;
-import androidx.loader.app.LoaderManager;
-
/**
* Base class for activities that want to use the support-based
* {@link Fragment Fragments}.
@@ -74,7 +65,7 @@
* specify an ID (or tag) in the <code><fragment></code>.</p>
* </ul>
*/
-public class FragmentActivity extends BaseFragmentActivityApi16 implements
+public class FragmentActivity extends SupportActivity implements
ViewModelStoreOwner,
ActivityCompat.OnRequestPermissionsResultCallback,
ActivityCompat.RequestPermissionsRequestCodeValidator {
@@ -120,6 +111,19 @@
boolean mRequestedPermissionsFromFragment;
+ // We need to keep track of whether startIntentSenderForResult originated from a Fragment, so we
+ // can conditionally check whether the requestCode collides with our reserved ID space for the
+ // request index (see above). Unfortunately we can't just call
+ // super.startIntentSenderForResult(...) to bypass the check when the call didn't come from a
+ // fragment, since we need to use the ActivityCompat version for backward compatibility.
+ boolean mStartedIntentSenderFromFragment;
+ // We need to keep track of whether startActivityForResult originated from a Fragment, so we
+ // can conditionally check whether the requestCode collides with our reserved ID space for the
+ // request index (see above). Unfortunately we can't just call
+ // super.startActivityForResult(...) to bypass the check when the call didn't come from a
+ // fragment, since we need to use the ActivityCompat version for backward compatibility.
+ boolean mStartedActivityFromFragment;
+
// A hint for the next candidate request index. Request indicies are ints between 0 and 2^16-1
// which are encoded into the upper 16 bits of the requestCode for
// Fragment.startActivityForResult(...) calls. This allows us to dispatch onActivityResult(...)
@@ -377,6 +381,23 @@
}
@Override
+ public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+ final View v = dispatchFragmentsOnCreateView(parent, name, context, attrs);
+ if (v == null) {
+ return super.onCreateView(parent, name, context, attrs);
+ }
+ return v;
+ }
+
+ @Override
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ final View v = dispatchFragmentsOnCreateView(null, name, context, attrs);
+ if (v == null) {
+ return super.onCreateView(name, context, attrs);
+ }
+ return v;
+ }
+
final View dispatchFragmentsOnCreateView(View parent, String name, Context context,
AttributeSet attrs) {
return mFragments.onCreateView(parent, name, context, attrs);
@@ -759,6 +780,59 @@
}
@Override
+ public void startActivityForResult(Intent intent, int requestCode,
+ @Nullable Bundle options) {
+ // If this was started from a Fragment we've already checked the upper 16 bits were not in
+ // use, and then repurposed them for the Fragment's index.
+ if (!mStartedActivityFromFragment) {
+ if (requestCode != -1) {
+ checkForValidRequestCode(requestCode);
+ }
+ }
+ super.startActivityForResult(intent, requestCode, options);
+ }
+
+ @Override
+ public void startIntentSenderForResult(IntentSender intent, int requestCode,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)
+ throws IntentSender.SendIntentException {
+ // If this was started from a Fragment we've already checked the upper 16 bits were not in
+ // use, and then repurposed them for the Fragment's index.
+ if (!mStartedIntentSenderFromFragment) {
+ if (requestCode != -1) {
+ checkForValidRequestCode(requestCode);
+ }
+ }
+ super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues,
+ extraFlags);
+ }
+
+ @Override
+ public void startIntentSenderForResult(IntentSender intent, int requestCode,
+ @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
+ Bundle options) throws IntentSender.SendIntentException {
+ // If this was started from a Fragment we've already checked the upper 16 bits were not in
+ // use, and then repurposed them for the Fragment's index.
+ if (!mStartedIntentSenderFromFragment) {
+ if (requestCode != -1) {
+ checkForValidRequestCode(requestCode);
+ }
+ }
+ super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues,
+ extraFlags, options);
+ }
+
+ /**
+ * Checks whether the given request code is a valid code by masking it with 0xffff0000. Throws
+ * an {@link IllegalArgumentException} if the code is not valid.
+ */
+ static void checkForValidRequestCode(int requestCode) {
+ if ((requestCode & 0xffff0000) != 0) {
+ throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
+ }
+ }
+
+ @Override
public final void validateRequestPermissionsRequestCode(int requestCode) {
// We use 16 bits of the request code to encode the fragment id when
// requesting permissions from a fragment. Hence, requestPermissions()
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index cd673d4..beb07f8 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -24,7 +24,6 @@
import android.animation.AnimatorSet;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
-import android.arch.lifecycle.ViewModelStore;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources.NotFoundException;
@@ -64,6 +63,7 @@
import androidx.core.util.LogWriter;
import androidx.core.util.Pair;
import androidx.core.view.ViewCompat;
+import androidx.lifecycle.ViewModelStore;
import java.io.FileDescriptor;
import java.io.PrintWriter;
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java b/fragment/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java
index c006f46..61553d6 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java
@@ -17,9 +17,10 @@
package androidx.fragment.app;
-import android.arch.lifecycle.ViewModelStore;
import android.os.Parcelable;
+import androidx.lifecycle.ViewModelStore;
+
import java.util.List;
/**
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentState.java b/fragment/src/main/java/androidx/fragment/app/FragmentState.java
index 2d94daf..d2fcb2a 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentState.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentState.java
@@ -16,13 +16,14 @@
package androidx.fragment.app;
-import android.arch.lifecycle.ViewModelStore;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
+import androidx.lifecycle.ViewModelStore;
+
final class FragmentState implements Parcelable {
final String mClassName;
final int mIndex;
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
index dcb6635..61415da 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/Processor.kt
@@ -85,7 +85,8 @@
fun createProcessor(
config: Config,
reversedMode: Boolean = false,
- rewritingSupportLib: Boolean = false
+ rewritingSupportLib: Boolean = false,
+ useIdentityIfTypeIsMissing: Boolean = true
): Processor {
var newConfig = config
@@ -101,7 +102,11 @@
)
}
- val context = TransformationContext(newConfig, rewritingSupportLib, reversedMode)
+ val context = TransformationContext(
+ config = newConfig,
+ rewritingSupportLib = rewritingSupportLib,
+ isInReversedMode = reversedMode,
+ useIdentityIfTypeIsMissing = useIdentityIfTypeIsMissing)
val transformers = if (rewritingSupportLib) {
createSLTransformers(context)
} else {
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
index e6739f0..3564ff3 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/archive/Archive.kt
@@ -111,18 +111,25 @@
object Builder {
+ /**
+ * @param recursive Whether nested archives should be also extracted.
+ */
@Throws(IOException::class)
- fun extract(archiveFile: File): Archive {
+ fun extract(archiveFile: File, recursive: Boolean = true): Archive {
Log.i(TAG, "Extracting: %s", archiveFile.absolutePath)
val inputStream = FileInputStream(archiveFile)
inputStream.use {
- return extractArchive(it, archiveFile.toPath())
+ return extractArchive(it, archiveFile.toPath(), recursive)
}
}
@Throws(IOException::class)
- private fun extractArchive(inputStream: InputStream, relativePath: Path): Archive {
+ private fun extractArchive(
+ inputStream: InputStream,
+ relativePath: Path,
+ recursive: Boolean
+ ): Archive {
val zipIn = ZipInputStream(inputStream)
val files = mutableListOf<ArchiveItem>()
@@ -131,9 +138,9 @@
while (entry != null) {
if (!entry.isDirectory) {
val entryPath = Paths.get(entry.name)
- if (isArchive(entry)) {
+ if (isArchive(entry) && recursive) {
Log.i(TAG, "Extracting nested: %s", entryPath)
- files.add(extractArchive(zipIn, entryPath))
+ files.add(extractArchive(zipIn, entryPath, recursive))
} else {
files.add(extractFile(zipIn, entryPath))
}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt
index 200f7f5..734f46d 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/rules/JavaType.kt
@@ -69,5 +69,16 @@
return JavaType(tokens.joinToString("$"))
}
+ /**
+ * Returns parent type of this types (e.g. for test.Class.InnerClass -> returns test.Class). For
+ * top level packages returns identity.
+ */
+ fun getParentType(): JavaType {
+ if (fullName.contains("/")) {
+ return JavaType(fullName.substringBeforeLast('/'))
+ }
+ return this
+ }
+
override fun toString() = fullName
}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
index 2b0d291..d6705af 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/TransformationContext.kt
@@ -28,7 +28,11 @@
class TransformationContext(
val config: Config,
val rewritingSupportLib: Boolean = false,
- val isInReversedMode: Boolean = false
+ val isInReversedMode: Boolean = false,
+ /**
+ * Whether to use identity if type in our scope is missing instead of throwing an exception.
+ */
+ val useIdentityIfTypeIsMissing: Boolean = true
) {
// Merges all packages prefixes into one regEx pattern
@@ -36,11 +40,6 @@
"^(" + config.restrictToPackagePrefixes.map { "($it)" }.joinToString("|") + ").*$")
/**
- * Whether to use identity if type in our scope is missing instead of throwing an exception.
- */
- val useIdentityIfTypeIsMissing = rewritingSupportLib || isInReversedMode
-
- /**
* Whether to skip verification of dependency version match in pom files.
*/
val ignorePomVersionCheck = rewritingSupportLib || isInReversedMode
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt
index 5a4a130..dc400e3 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImpl.kt
@@ -66,19 +66,36 @@
}
override fun rewriteString(value: String): String {
+ val startsWithAndroidX = context.isInReversedMode && value.startsWith("androidx")
+
val type = JavaType.fromDotVersion(value)
if (!context.isEligibleForRewrite(type)) {
+ if (startsWithAndroidX) {
+ Log.i(TAG, "Found string '%s' but failed to rewrite", value)
+ }
return value
}
val result = typesMap.mapType(type)
if (result != null) {
changesDone = changesDone || result != type
- Log.i(TAG, " map string: %s -> %s", type, result)
+ Log.i(TAG, "Map string: '%s' -> '%s'", type, result)
return result.toDotNotation()
}
+ // We might be working with an internal type or field reference, e.g.
+ // AccessibilityNodeInfoCompat.PANE_TITLE_KEY. So we try to remove last segment to help it.
+ if (value.contains(".")) {
+ val subTypeResult = typesMap.mapType(type.getParentType())
+ if (subTypeResult != null) {
+ val result = subTypeResult.toDotNotation() + '.' + value.substringAfterLast('.')
+ Log.i(TAG, "Map string: '%s' -> '%s' via fallback", value, result)
+ return result
+ }
+ }
+
// We do not treat string content mismatches as errors
+ Log.i(TAG, "Found string '%s' but failed to rewrite", value)
return value
}
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt
index f923198..1c10e02 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/pom/PomDocument.kt
@@ -80,6 +80,11 @@
* Changes are not saved back until requested.
*/
fun applyRules(context: TransformationContext) {
+ if (context.rewritingSupportLib) {
+ rewriteOwnArtifactInfo(context)
+ hasChanged = true
+ }
+
if (dependenciesGroup == null) {
// Nothing to transform as this file has no dependencies section
return
@@ -87,32 +92,10 @@
val newDependencies = mutableSetOf<PomDependency>()
for (dependency in dependencies) {
- if (dependency.shouldSkipRewrite()) {
- continue
- }
-
- val rule = context.config.pomRewriteRules.firstOrNull { it.matches(dependency) }
- if (rule != null) {
- // Replace with new dependencies
- newDependencies.addAll(rule.to.mapTo(newDependencies) { it.rewrite(dependency) })
- continue
- }
-
- val matchesPrefix = context.config.restrictToPackagePrefixesWithDots.any {
- dependency.groupId!!.startsWith(it)
- }
- if (matchesPrefix) {
- // Report error
- Log.e(TAG, "No mapping found for '%s'", dependency.toStringNotation())
- context.reportNoPackageMappingFoundFailure()
- }
-
- // No rule to rewrite => keep it
- newDependencies.add(dependency)
+ newDependencies.addAll(mapDependency(dependency, context))
}
if (newDependencies.isEmpty()) {
- // No changes
return
}
@@ -121,6 +104,72 @@
hasChanged = true
}
+ fun getAsPomDependency(): PomDependency {
+ val groupIdNode = document.rootElement
+ .getChild("groupId", document.rootElement.namespace)
+ val artifactIdNode = document.rootElement
+ .getChild("artifactId", document.rootElement.namespace)
+ val version = document.rootElement
+ .getChild("version", document.rootElement.namespace)
+
+ return PomDependency(groupIdNode.text, artifactIdNode.text, version.text)
+ }
+
+ private fun rewriteOwnArtifactInfo(context: TransformationContext) {
+ val groupIdNode = document.rootElement
+ .getChild("groupId", document.rootElement.namespace)
+ val artifactIdNode = document.rootElement
+ .getChild("artifactId", document.rootElement.namespace)
+ val version = document.rootElement
+ .getChild("version", document.rootElement.namespace)
+
+ if (groupIdNode == null || artifactIdNode == null || version == null) {
+ return
+ }
+
+ val dependency = PomDependency(groupIdNode.text, artifactIdNode.text, version.text)
+ val newDependency = mapDependency(dependency, context).first()
+
+ if (newDependency != dependency) {
+ groupIdNode.text = newDependency.groupId
+ artifactIdNode.text = newDependency.artifactId
+ version.text = newDependency.version
+ }
+ }
+
+ private fun mapDependency(
+ dependency: PomDependency,
+ context: TransformationContext
+ ): Set<PomDependency> {
+ if (dependency.shouldSkipRewrite()) {
+ return emptySet()
+ }
+
+ val rule = context.config.pomRewriteRules.firstOrNull { it.matches(dependency) }
+ if (rule != null) {
+ // Replace with new dependencies
+ return rule.to.map { it.rewrite(dependency) }.toSet()
+ }
+
+ val matchesPrefix = context.config.restrictToPackagePrefixesWithDots.any {
+ dependency.groupId!!.startsWith(it)
+ }
+
+ if (matchesPrefix) {
+ if (context.useIdentityIfTypeIsMissing) {
+ Log.i(TAG, "No mapping found for '%s' - using identity",
+ dependency.toStringNotation())
+ } else {
+ // Report error
+ Log.e(TAG, "No mapping found for '%s'", dependency.toStringNotation())
+ context.reportNoPackageMappingFoundFailure()
+ }
+ }
+
+ // No rule to rewrite => keep it
+ return setOf(dependency)
+ }
+
/**
* Saves any current pending changes back to the file if needed.
*/
diff --git a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt
index dc63495..c7c6b1d 100644
--- a/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt
+++ b/jetifier/jetifier/core/src/main/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformer.kt
@@ -183,8 +183,14 @@
if (result != null) {
return result.toDotNotation()
}
+ if (context.useIdentityIfTypeIsMissing) {
+ Log.i(TAG, "No mapping for package: '%s' in artifact: '%s' - using identity",
+ pckg, archiveName)
+ return packageName
+ }
+
context.reportNoPackageMappingFoundFailure()
- Log.e(TAG, "No mapping for package: '$pckg' in artifact: '$archiveName'")
+ Log.e(TAG, "No mapping for package: '%s' in artifact: '%s'", pckg, archiveName)
return packageName
}
}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/main/resources/default.config b/jetifier/jetifier/core/src/main/resources/default.config
index 26f642a..748aa8f 100644
--- a/jetifier/jetifier/core/src/main/resources/default.config
+++ b/jetifier/jetifier/core/src/main/resources/default.config
@@ -420,10 +420,6 @@
to: "androidx/media/AudioAttributesCompat{0}"
},
{
- from: "android/support/v4/media/MediaBrowserCompat(.*)",
- to: "androidx/media/MediaBrowserCompat{0}"
- },
- {
from: "android/support/v4/media/MediaBrowserCompatUtils(.*)",
to: "androidx/media/MediaBrowserCompatUtils{0}"
},
@@ -444,10 +440,6 @@
to: "androidx/media/session/MediaButtonReceiver{0}"
},
{
- from: "android/support/v4/media/session/MediaControllerCompat(.*)",
- to: "androidx/media/session/MediaControllerCompat{0}"
- },
- {
from: "android/support/v4/media/(.*)",
to: "android/support/v4/media/{0}"
},
@@ -831,7 +823,7 @@
to: [{ groupId: "androidx.asynclayoutinflater", artifactId: "asynclayoutinflater", version: "1.0.0" }]
},
{
- from: { groupId: "com.android.support", artifactId: "collection", version: "28.0.0" },
+ from: { groupId: "com.android.support", artifactId: "collections", version: "28.0.0" },
to: [{ groupId: "androidx.collection", artifactId: "collection", version: "1.0.0" }]
},
{
diff --git a/jetifier/jetifier/core/src/main/resources/default.generated.config b/jetifier/jetifier/core/src/main/resources/default.generated.config
index 5491498..2fd93ad 100644
--- a/jetifier/jetifier/core/src/main/resources/default.generated.config
+++ b/jetifier/jetifier/core/src/main/resources/default.generated.config
@@ -1187,7 +1187,7 @@
{
"from": {
"groupId": "com.android.support",
- "artifactId": "collection",
+ "artifactId": "collections",
"version": "28.0.0"
},
"to": [
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/rules/JavaTypeTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/rules/JavaTypeTest.kt
new file mode 100644
index 0000000..a5c0e89
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/rules/JavaTypeTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 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.support.tools.jetifier.core.rules
+
+import com.google.common.truth.Truth
+import org.junit.Test
+
+class JavaTypeTest {
+ @Test fun javaType_testFromDotVersion() {
+ val type = JavaType.fromDotVersion("test.MyClass.FIELD")
+
+ Truth.assertThat(type.fullName).isEqualTo("test/MyClass/FIELD")
+ }
+
+ @Test fun javaType_testParent() {
+ val type = JavaType.fromDotVersion("test.MyClass.FIELD")
+ val result = type.getParentType().toDotNotation()
+
+ Truth.assertThat(result).isEqualTo("test.MyClass")
+ }
+
+ @Test fun javaType_testParent_identity() {
+ val type = JavaType.fromDotVersion("test")
+ val result = type.getParentType().toDotNotation()
+
+ Truth.assertThat(result).isEqualTo("test")
+ }
+
+ @Test fun javaType_remapeWithNewRootType() {
+ val type = JavaType.fromDotVersion("test.MyClass\$Inner")
+ val remapWith = JavaType.fromDotVersion("hello.NewClass")
+
+ Truth.assertThat(type.remapWithNewRootType(remapWith).toDotNotation())
+ .isEqualTo("hello.NewClass\$Inner")
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImplTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImplTest.kt
new file mode 100644
index 0000000..4dcbd39
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/bytecode/CoreRemapperImplTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018 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.support.tools.jetifier.core.transform.bytecode
+
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.rules.JavaType
+import android.support.tools.jetifier.core.transform.PackageMap
+import android.support.tools.jetifier.core.transform.TransformationContext
+import android.support.tools.jetifier.core.transform.proguard.ProGuardTypesMap
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.objectweb.asm.ClassWriter
+
+class CoreRemapperImplTest {
+
+ @Test
+ fun remapString_shouldUseFallbackForField() {
+ val remapper = prepareRemapper(
+ TypesMap(mapOf(
+ JavaType.fromDotVersion("androidx.test.InputConnectionCompat")
+ to JavaType.fromDotVersion("android.support.test.InputConnectionCompat")
+ )),
+ "androidx/")
+
+ val given = "androidx.test.InputConnectionCompat.CONTENT_URI"
+ val expected = "android.support.test.InputConnectionCompat.CONTENT_URI"
+
+ Truth.assertThat(remapper.rewriteString(given)).isEqualTo(expected)
+ }
+
+ private fun prepareRemapper(
+ typesMap: TypesMap,
+ restrictToPackagePrefix: String? = null
+ ): CoreRemapperImpl {
+ val prefixes = if (restrictToPackagePrefix == null) {
+ emptyList()
+ } else {
+ listOf(restrictToPackagePrefix)
+ }
+
+ val config = Config(
+ restrictToPackagePrefixes = prefixes,
+ rewriteRules = emptyList(),
+ typesMap = typesMap,
+ slRules = emptyList(),
+ pomRewriteRules = emptySet(),
+ proGuardMap = ProGuardTypesMap.EMPTY,
+ packageMap = PackageMap.EMPTY)
+
+ val context = TransformationContext(config,
+ isInReversedMode = true)
+
+ val writer = ClassWriter(0 /* flags */)
+ return CoreRemapperImpl(context, writer)
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteInZipTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteInZipTest.kt
new file mode 100644
index 0000000..77dc196
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/pom/PomRewriteInZipTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018 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.support.tools.jetifier.core.transform.pom
+
+import android.support.tools.jetifier.core.FileMapping
+import android.support.tools.jetifier.core.Processor
+import android.support.tools.jetifier.core.archive.Archive
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.config.Config
+import android.support.tools.jetifier.core.map.TypesMap
+import android.support.tools.jetifier.core.transform.proguard.ProGuardTypesMap
+import com.google.common.truth.Truth
+import org.junit.Test
+import java.io.File
+
+/**
+ * Tests that pom file inside a zip file was correctly rewritten.
+ */
+class PomRewriteInZipTest {
+
+ companion object {
+ private val TEST_CONFIG = Config(
+ restrictToPackagePrefixes = listOf("com/sample"),
+ rewriteRules = listOf(),
+ slRules = listOf(),
+ pomRewriteRules = setOf(
+ PomRewriteRule(
+ from = PomDependency(
+ groupId = "old.group",
+ artifactId = "myOldArtifact",
+ version = "0.1.0"),
+ to = setOf(
+ PomDependency(
+ groupId = "com.sample.my.group",
+ artifactId = "myArtifact",
+ version = "1.0.0"
+ )
+ )
+ )),
+ typesMap = TypesMap.EMPTY,
+ proGuardMap = ProGuardTypesMap.EMPTY
+ )
+ }
+
+ @Test fun rewritePomInZip_rewritingSL_shouldRewrite() {
+ val inputZipPath = "/pomRefactorTest/pomTest.zip"
+
+ val processor = Processor.createProcessor(
+ TEST_CONFIG,
+ rewritingSupportLib = true,
+ reversedMode = true)
+
+ val inputFile = File(javaClass.getResource(inputZipPath).file)
+
+ val tempDir = createTempDir()
+ val expectedFile = File(createTempDir(), "test.zip")
+
+ val resultFiles = processor.transform(setOf(FileMapping(inputFile, expectedFile)))
+
+ Truth.assertThat(resultFiles).hasSize(1)
+
+ val returnedArchive = Archive.Builder.extract(expectedFile)
+ val returnedPom = returnedArchive.files.first() as ArchiveFile
+ val content = String(returnedPom.data)
+
+ Truth.assertThat(returnedPom.fileName).isEqualTo("test.pom")
+
+ Truth.assertThat(content).doesNotContain("com.sample.my.group")
+ Truth.assertThat(content).doesNotContain("myArtifact")
+ Truth.assertThat(content).doesNotContain("1.0.0")
+
+ Truth.assertThat(content).contains("old.group")
+ Truth.assertThat(content).contains("myOldArtifact")
+ Truth.assertThat(content).contains("0.1.0")
+
+ tempDir.delete()
+ }
+
+ @Test fun rewritePomInZip_notRewritingSL_shouldNotRewrite() {
+ val inputZipPath = "/pomRefactorTest/pomTest.zip"
+
+ val processor = Processor.createProcessor(
+ TEST_CONFIG,
+ rewritingSupportLib = false,
+ reversedMode = true)
+
+ val inputFile = File(javaClass.getResource(inputZipPath).file)
+
+ val tempDir = createTempDir()
+ val expectedFile = File(createTempDir(), "test.zip")
+
+ val resultFiles = processor.transform(setOf(FileMapping(inputFile, expectedFile)))
+
+ Truth.assertThat(resultFiles).hasSize(1)
+
+ val returnedArchive = Archive.Builder.extract(expectedFile)
+ val returnedPom = returnedArchive.files.first() as ArchiveFile
+ val content = String(returnedPom.data)
+
+ Truth.assertThat(returnedPom.fileName).isEqualTo("test.pom")
+
+ Truth.assertThat(content).contains("com.sample.my.group")
+ Truth.assertThat(content).contains("myArtifact")
+ Truth.assertThat(content).contains("1.0.0")
+
+ Truth.assertThat(content).doesNotContain("old.group")
+ Truth.assertThat(content).doesNotContain("myOldArtifact")
+ Truth.assertThat(content).doesNotContain("0.1.0")
+
+ tempDir.delete()
+ }
+}
\ No newline at end of file
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
index d4acfb6..7d13826 100644
--- a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/proguard/ProGuardTester.kt
@@ -100,7 +100,7 @@
class ProGuardTesterForType(private val config: Config, private val given: String) {
fun getsRewrittenTo(expectedType: String) {
- val context = TransformationContext(config)
+ val context = TransformationContext(config, useIdentityIfTypeIsMissing = false)
val mapper = ProGuardTypesMapper(context)
val result = mapper.replaceType(given)
diff --git a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
index 44f2ba4..3549559 100644
--- a/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
+++ b/jetifier/jetifier/core/src/test/kotlin/android/support/tools/jetifier/core/transform/resource/XmlResourcesTransformerTest.kt
@@ -330,7 +330,10 @@
proGuardMap = ProGuardTypesMap.EMPTY,
packageMap = packageMap
)
- val context = TransformationContext(config, rewritingSupportLib = rewritingSupportLib)
+ val context = TransformationContext(
+ config,
+ rewritingSupportLib = rewritingSupportLib,
+ useIdentityIfTypeIsMissing = false)
context.libraryName = libraryName
val processor = XmlResourcesTransformer(context)
val fileName = if (isManifestFile) {
diff --git a/jetifier/jetifier/core/src/test/resources/pomRefactorTest/pomTest.zip b/jetifier/jetifier/core/src/test/resources/pomRefactorTest/pomTest.zip
new file mode 100644
index 0000000..4b2270b
--- /dev/null
+++ b/jetifier/jetifier/core/src/test/resources/pomRefactorTest/pomTest.zip
Binary files differ
diff --git a/jetifier/jetifier/standalone/build.gradle b/jetifier/jetifier/standalone/build.gradle
index 3caa366..eed727c 100644
--- a/jetifier/jetifier/standalone/build.gradle
+++ b/jetifier/jetifier/standalone/build.gradle
@@ -31,5 +31,5 @@
destinationDir rootProject.distDir // defined by support library plugin
}
-rootProject.tasks["buildOnServer"].dependsOn(dist)
+rootProject.tasks["createArchive"].dependsOn(dist)
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
index e840a01..e426621 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/Main.kt
@@ -43,13 +43,17 @@
val OPTION_OUTPUT_FILE = createOption("outputfile", "Output file", isRequired = false)
val OPTION_CONFIG = createOption("c", "Input config path", isRequired = false)
val OPTION_LOG_LEVEL = createOption("l", "Logging level. debug, verbose, error, info " +
- "(default)", isRequired = false)
+ "(default)", isRequired = false)
val OPTION_REVERSED = createOption("r", "Run reversed process", hasArgs = false,
- isRequired = false)
+ isRequired = false)
val OPTION_REWRITE_SUPPORT_LIB = createOption("s", "If set, all libraries being rewritten" +
- " are assumed to be part of Support Library. Otherwise only general dependencies are" +
- " expected.",
- hasArgs = false, isRequired = false)
+ " are assumed to be part of Support Library. Otherwise only general dependencies " +
+ " are expected.", hasArgs = false, isRequired = false)
+ val OPTION_STRICT = createOption("strict",
+ "Don't fallback in case rules are missing", hasArgs = false, isRequired = false)
+ val OPTION_REBUILD_TOP_OF_TREE = createOption("rebuildTopOfTree",
+ "Rebuild the zip of maven distribution according to the generated pom file",
+ hasArgs = false, isRequired = false)
private fun createOption(
argName: String,
@@ -80,6 +84,8 @@
val inputLibraries = cmd.getOptionValues(OPTION_INPUT.opt).map { File(it) }.toSet()
val outputDir = cmd.getOptionValue(OPTION_OUTPUT_DIR.opt)
val outputFile = cmd.getOptionValue(OPTION_OUTPUT_FILE.opt)
+ val rebuildTopOfTree = cmd.hasOption(OPTION_REBUILD_TOP_OF_TREE.opt)
+
if (outputDir == null && outputFile == null) {
throw IllegalArgumentException("Must specify -outputdir or -outputfile")
}
@@ -93,8 +99,12 @@
val fileMappings = mutableSetOf<FileMapping>()
if (outputFile != null) {
- fileMappings.add(FileMapping(inputLibraries.first(),
- File(outputFile)))
+ if (rebuildTopOfTree) {
+ val tempFile = createTempFile(suffix = "zip")
+ fileMappings.add(FileMapping(inputLibraries.first(), tempFile))
+ } else {
+ fileMappings.add(FileMapping(inputLibraries.first(), File(outputFile)))
+ }
} else {
inputLibraries.forEach {
val newFileName = File(Paths.get(outputDir).toString(), it.name)
@@ -118,11 +128,21 @@
val isReversed = cmd.hasOption(OPTION_REVERSED.opt)
val rewriteSupportLib = cmd.hasOption(OPTION_REWRITE_SUPPORT_LIB.opt)
+ val isStrict = cmd.hasOption(OPTION_STRICT.opt)
val processor = Processor.createProcessor(
config = config,
reversedMode = isReversed,
- rewritingSupportLib = rewriteSupportLib)
+ rewritingSupportLib = rewriteSupportLib,
+ useIdentityIfTypeIsMissing = !isStrict)
processor.transform(fileMappings)
+
+ if (rebuildTopOfTree) {
+ val tempFile = fileMappings.first().to
+ TopOfTreeBuilder().rebuildFrom(
+ inputZip = tempFile,
+ outputZip = File(outputFile))
+ tempFile.delete()
+ }
}
private fun parseCmdLine(args: Array<String>): CommandLine? {
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/TopOfTreeBuilder.kt b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/TopOfTreeBuilder.kt
new file mode 100644
index 0000000..893ee65
--- /dev/null
+++ b/jetifier/jetifier/standalone/src/main/kotlin/android/support/tools/jetifier/standalone/TopOfTreeBuilder.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018 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.support.tools.jetifier.standalone
+
+import android.support.tools.jetifier.core.archive.Archive
+import android.support.tools.jetifier.core.archive.ArchiveFile
+import android.support.tools.jetifier.core.archive.ArchiveItemVisitor
+import android.support.tools.jetifier.core.transform.pom.PomDocument
+import java.io.File
+import java.nio.file.Paths
+import java.security.MessageDigest
+
+/**
+ * Utility to rebuild de-jetified zipped maven repo.
+ */
+class TopOfTreeBuilder {
+
+ companion object {
+ const val DIR_PREFIX = "m2repository"
+ }
+
+ fun rebuildFrom(inputZip: File, outputZip: File) {
+ val archive = Archive.Builder.extract(inputZip, recursive = false)
+
+ // Find poms
+ val pomFilter = FileFilter({ it.isPomFile() })
+ archive.accept(pomFilter)
+ val pomFiles = pomFilter.files
+
+ // Find archives
+ val archivesFilter = FileFilter({
+ return@FileFilter it.fileName.endsWith(".aar")
+ || (it.fileName.endsWith("jar")
+ && !it.fileName.contains("sources") && !it.fileName.contains("javadoc"))
+ })
+ archive.accept(archivesFilter)
+ val libFiles = archivesFilter.files
+
+ // Process
+ val newFiles = mutableSetOf<ArchiveFile>()
+ pomFiles.forEach {
+ pomFile -> run {
+ val name = pomFile.relativePath.toFile().nameWithoutExtension
+ val artifactFile = libFiles.first { it.relativePath.toString().contains(name) }
+ process(pomFile, artifactFile, newFiles)
+ }
+ }
+
+ // Write the result
+ val finalArchive = Archive(outputZip.toPath(), newFiles.toList())
+ finalArchive.writeSelf()
+ }
+
+ private fun process(
+ pomFile: ArchiveFile,
+ artifactFile: ArchiveFile,
+ resultSet: MutableSet<ArchiveFile>
+ ) {
+ val pomDep = PomDocument.loadFrom(pomFile).getAsPomDependency()
+
+ val groupAsPath = pomDep.groupId!!.replace('.', '/')
+
+ val packaging = artifactFile.relativePath.toFile().extension
+ val baseFileName = "${pomDep.artifactId}-${pomDep.version}"
+
+ val artifactDir = Paths.get(
+ DIR_PREFIX, groupAsPath, pomDep.artifactId, pomDep.version!!.toUpperCase())
+ val newLibFilePath = Paths.get(artifactDir.toString(), "$baseFileName.$packaging")
+ val newPomFilePath = Paths.get(artifactDir.toString(), "$baseFileName.pom")
+
+ val newArtifactFile = ArchiveFile(newLibFilePath, artifactFile.data)
+ val newPomFile = ArchiveFile(newPomFilePath, pomFile.data)
+
+ resultSet.add(newArtifactFile)
+ resultSet.add(getHashFileOf(newArtifactFile, "MD5"))
+ resultSet.add(getHashFileOf(newArtifactFile, "SHA1"))
+
+ resultSet.add(newPomFile)
+ resultSet.add(getHashFileOf(newPomFile, "MD5"))
+ resultSet.add(getHashFileOf(newPomFile, "SHA1"))
+ }
+
+ private fun getHashFileOf(file: ArchiveFile, hashType: String): ArchiveFile {
+ val md = MessageDigest.getInstance(hashType)
+ val result = md.digest(file.data)
+ return ArchiveFile(Paths.get(
+ file.relativePath.toString() + "." + hashType.toLowerCase()), result)
+ }
+
+ private class FileFilter(private val filter: (ArchiveFile) -> Boolean) : ArchiveItemVisitor {
+
+ val files = mutableSetOf<ArchiveFile>()
+
+ override fun visit(archiveFile: ArchiveFile) {
+ if (filter(archiveFile)) {
+ files.add(archiveFile)
+ }
+ }
+
+ override fun visit(archive: Archive) {
+ archive.files.forEach { it.accept(this) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/leanback/api/current.txt b/leanback/api/current.txt
index e13df6d..138509e 100644
--- a/leanback/api/current.txt
+++ b/leanback/api/current.txt
@@ -1004,9 +1004,9 @@
package androidx.leanback.media {
public class MediaControllerAdapter extends androidx.leanback.media.PlayerAdapter {
- ctor public MediaControllerAdapter(androidx.media.session.MediaControllerCompat);
+ ctor public MediaControllerAdapter(android.support.v4.media.session.MediaControllerCompat);
method public android.graphics.drawable.Drawable getMediaArt(android.content.Context);
- method public androidx.media.session.MediaControllerCompat getMediaController();
+ method public android.support.v4.media.session.MediaControllerCompat getMediaController();
method public java.lang.CharSequence getMediaSubtitle();
method public java.lang.CharSequence getMediaTitle();
method public void pause();
@@ -1015,12 +1015,12 @@
public abstract deprecated class MediaControllerGlue extends androidx.leanback.media.PlaybackControlGlue {
ctor public MediaControllerGlue(android.content.Context, int[], int[]);
- method public void attachToMediaController(androidx.media.session.MediaControllerCompat);
+ method public void attachToMediaController(android.support.v4.media.session.MediaControllerCompat);
method public void detach();
method public int getCurrentPosition();
method public int getCurrentSpeedId();
method public android.graphics.drawable.Drawable getMediaArt();
- method public final androidx.media.session.MediaControllerCompat getMediaController();
+ method public final android.support.v4.media.session.MediaControllerCompat getMediaController();
method public int getMediaDuration();
method public java.lang.CharSequence getMediaSubtitle();
method public java.lang.CharSequence getMediaTitle();
diff --git a/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java b/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java
index 0baeb9f..eba9864 100644
--- a/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java
+++ b/leanback/src/androidTest/java/androidx/leanback/media/MediaControllerAdapterTest.java
@@ -34,11 +34,11 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.leanback.widget.PlaybackControlsRow;
-import androidx.media.session.MediaControllerCompat;
import org.junit.After;
import org.junit.Before;
diff --git a/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java b/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
index 16d78c4..e4fe997 100644
--- a/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
+++ b/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
@@ -30,11 +30,11 @@
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import androidx.leanback.widget.PlaybackControlsRow;
-import androidx.media.session.MediaControllerCompat;
/**
* A helper class for implementing a adapter layer for {@link MediaControllerCompat}.
diff --git a/leanback/src/main/java/androidx/leanback/media/MediaControllerGlue.java b/leanback/src/main/java/androidx/leanback/media/MediaControllerGlue.java
index f9cdefc..2a7977c 100644
--- a/leanback/src/main/java/androidx/leanback/media/MediaControllerGlue.java
+++ b/leanback/src/main/java/androidx/leanback/media/MediaControllerGlue.java
@@ -22,11 +22,10 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
-import androidx.media.session.MediaControllerCompat;
-
/**
* A helper class for implementing a glue layer for {@link MediaControllerCompat}.
* @deprecated Use {@link MediaControllerAdapter} with {@link PlaybackTransportControlGlue} or
diff --git a/lifecycle/common-java8/api/1.0.0.txt b/lifecycle/common-java8/api_legacy/1.0.0.txt
similarity index 100%
rename from lifecycle/common-java8/api/1.0.0.txt
rename to lifecycle/common-java8/api_legacy/1.0.0.txt
diff --git a/lifecycle/common-java8/build.gradle b/lifecycle/common-java8/build.gradle
index 0f4ffd8..205ed87 100644
--- a/lifecycle/common-java8/build.gradle
+++ b/lifecycle/common-java8/build.gradle
@@ -24,7 +24,7 @@
}
dependencies {
- compile(project(":lifecycle:common"))
+ compile(project(":lifecycle:lifecycle-common"))
compile(SUPPORT_ANNOTATIONS)
testCompile(JUNIT)
diff --git a/lifecycle/common-java8/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.java b/lifecycle/common-java8/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.java
deleted file mode 100644
index b6f468c..0000000
--- a/lifecycle/common-java8/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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/lifecycle/common-java8/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.java b/lifecycle/common-java8/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.java
new file mode 100644
index 0000000..f10decb
--- /dev/null
+++ b/lifecycle/common-java8/src/main/java/androidx/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 androidx.lifecycle;
+
+import androidx.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/lifecycle/common/api/0.0.0.txt b/lifecycle/common/api_legacy/0.0.0.txt
similarity index 100%
rename from lifecycle/common/api/0.0.0.txt
rename to lifecycle/common/api_legacy/0.0.0.txt
diff --git a/lifecycle/common/api/1.0.0.txt b/lifecycle/common/api_legacy/1.0.0.txt
similarity index 100%
rename from lifecycle/common/api/1.0.0.txt
rename to lifecycle/common/api_legacy/1.0.0.txt
diff --git a/lifecycle/common/api/current.txt b/lifecycle/common/api_legacy/current.txt
similarity index 100%
rename from lifecycle/common/api/current.txt
rename to lifecycle/common/api_legacy/current.txt
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java b/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java
deleted file mode 100644
index d88e276..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * 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 = getDeclaredMethods(klass);
- 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;
- }
-
- private Method[] getDeclaredMethods(Class klass) {
- try {
- return klass.getDeclaredMethods();
- } catch (NoClassDefFoundError e) {
- throw new IllegalArgumentException("The observer class has some methods that use "
- + "newer APIs which are not available in the current OS version. Lifecycles "
- + "cannot access even other methods so you should make sure that your "
- + "observer classes only access framework classes that are available "
- + "in your min API level OR use lifecycle:compiler annotation processor.", e);
- }
- }
-
- 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 : getDeclaredMethods(klass);
- 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/lifecycle/common/src/main/java/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java
deleted file mode 100644
index e8cbe7c..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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/lifecycle/common/src/main/java/android/arch/lifecycle/FullLifecycleObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/FullLifecycleObserver.java
deleted file mode 100644
index f179274..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/FullLifecycleObserver.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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/lifecycle/common/src/main/java/android/arch/lifecycle/FullLifecycleObserverAdapter.java b/lifecycle/common/src/main/java/android/arch/lifecycle/FullLifecycleObserverAdapter.java
deleted file mode 100644
index 0a91a66..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/FullLifecycleObserverAdapter.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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/lifecycle/common/src/main/java/android/arch/lifecycle/GeneratedAdapter.java b/lifecycle/common/src/main/java/android/arch/lifecycle/GeneratedAdapter.java
deleted file mode 100644
index a8862da..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/GeneratedAdapter.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java
deleted file mode 100644
index 4601478..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/GenericLifecycleObserver.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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;
-
-/**
- * Internal class that can receive any lifecycle change and dispatch it to the receiver.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings({"WeakerAccess", "unused"})
-public interface GenericLifecycleObserver extends LifecycleObserver {
- /**
- * Called when a state transition event happens.
- *
- * @param source The source of the event
- * @param event The event
- */
- void onStateChanged(LifecycleOwner source, Lifecycle.Event event);
-}
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/Lifecycle.java b/lifecycle/common/src/main/java/android/arch/lifecycle/Lifecycle.java
deleted file mode 100644
index e04cdb4..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/Lifecycle.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * 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.MainThread;
-import android.support.annotation.NonNull;
-
-/**
- * Defines an object that has an Android Lifecycle. {@link android.support.v4.app.Fragment Fragment}
- * and {@link android.support.v4.app.FragmentActivity FragmentActivity} classes implement
- * {@link LifecycleOwner} interface which has the {@link LifecycleOwner#getLifecycle()
- * getLifecycle} method to access the Lifecycle. You can also implement {@link LifecycleOwner}
- * in your own classes.
- * <p>
- * {@link Event#ON_CREATE}, {@link Event#ON_START}, {@link Event#ON_RESUME} events in this class
- * are dispatched <b>after</b> the {@link LifecycleOwner}'s related method returns.
- * {@link Event#ON_PAUSE}, {@link Event#ON_STOP}, {@link Event#ON_DESTROY} events in this class
- * are dispatched <b>before</b> the {@link LifecycleOwner}'s related method is called.
- * For instance, {@link Event#ON_START} will be dispatched after
- * {@link android.app.Activity#onStart onStart} returns, {@link Event#ON_STOP} will be dispatched
- * before {@link android.app.Activity#onStop onStop} is called.
- * This gives you certain guarantees on which state the owner is in.
- * <p>
- * 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)
- * void onStopped() {}
- * }
- * </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
- * of type {@link Event}.
- * <pre>
- * class TestObserver implements LifecycleObserver {
- * {@literal @}OnLifecycleEvent(ON_CREATE)
- * void onCreated(LifecycleOwner source) {}
- * {@literal @}OnLifecycleEvent(ON_ANY)
- * void onAny(LifecycleOwner source, Event event) {}
- * }
- * </pre>
- * These additional parameters are provided to allow you to conveniently observe multiple providers
- * and events without tracking them manually.
- */
-public abstract class Lifecycle {
- /**
- * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
- * state.
- * <p>
- * The given observer will be brought to the current state of the LifecycleOwner.
- * For example, if the LifecycleOwner is in {@link State#STARTED} state, the given observer
- * will receive {@link Event#ON_CREATE}, {@link Event#ON_START} events.
- *
- * @param observer The observer to notify.
- */
- @MainThread
- public abstract void addObserver(@NonNull LifecycleObserver observer);
-
- /**
- * Removes the given observer from the observers list.
- * <p>
- * If this method is called while a state change is being dispatched,
- * <ul>
- * <li>If the given observer has not yet received that event, it will not receive it.
- * <li>If the given observer has more than 1 method that observes the currently dispatched
- * event and at least one of them received the event, all of them will receive the event and
- * the removal will happen afterwards.
- * </ul>
- *
- * @param observer The observer to be removed.
- */
- @MainThread
- public abstract void removeObserver(@NonNull LifecycleObserver observer);
-
- /**
- * Returns the current state of the Lifecycle.
- *
- * @return The current state of the Lifecycle.
- */
- @MainThread
- @NonNull
- public abstract State getCurrentState();
-
- @SuppressWarnings("WeakerAccess")
- public enum Event {
- /**
- * Constant for onCreate event of the {@link LifecycleOwner}.
- */
- ON_CREATE,
- /**
- * Constant for onStart event of the {@link LifecycleOwner}.
- */
- ON_START,
- /**
- * Constant for onResume event of the {@link LifecycleOwner}.
- */
- ON_RESUME,
- /**
- * Constant for onPause event of the {@link LifecycleOwner}.
- */
- ON_PAUSE,
- /**
- * Constant for onStop event of the {@link LifecycleOwner}.
- */
- ON_STOP,
- /**
- * Constant for onDestroy event of the {@link LifecycleOwner}.
- */
- ON_DESTROY,
- /**
- * An {@link Event Event} constant that can be used to match all events.
- */
- ON_ANY
- }
-
- /**
- * Lifecycle states. You can consider the states as the nodes in a graph and
- * {@link Event}s as the edges between these nodes.
- */
- @SuppressWarnings("WeakerAccess")
- public enum State {
- /**
- * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
- * any more events. For instance, for an {@link android.app.Activity}, this state is reached
- * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
- */
- DESTROYED,
-
- /**
- * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
- * the state when it is constructed but has not received
- * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
- */
- INITIALIZED,
-
- /**
- * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
- * is reached in two cases:
- * <ul>
- * <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
- * <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
- * </ul>
- */
- CREATED,
-
- /**
- * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
- * is reached in two cases:
- * <ul>
- * <li>after {@link android.app.Activity#onStart() onStart} call;
- * <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
- * </ul>
- */
- STARTED,
-
- /**
- * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
- * is reached after {@link android.app.Activity#onResume() onResume} is called.
- */
- RESUMED;
-
- /**
- * Compares if this State is greater or equal to the given {@code state}.
- *
- * @param state State to compare with
- * @return true if this State is greater or equal to the given {@code state}
- */
- public boolean isAtLeast(@NonNull State state) {
- return compareTo(state) >= 0;
- }
- }
-}
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/LifecycleObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/LifecycleObserver.java
deleted file mode 100644
index 94670bf..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/LifecycleObserver.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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;
-
-/**
- * Marks a class as a LifecycleObserver. It does not have any methods, instead, relies on
- * {@link OnLifecycleEvent} annotated methods.
- * <p>
- * @see Lifecycle Lifecycle - for samples and usage patterns.
- */
-@SuppressWarnings("WeakerAccess")
-public interface LifecycleObserver {
-
-}
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/LifecycleOwner.java b/lifecycle/common/src/main/java/android/arch/lifecycle/LifecycleOwner.java
deleted file mode 100644
index 068bac1..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/LifecycleOwner.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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;
-
-/**
- * A class that has an Android lifecycle. These events can be used by custom components to
- * handle lifecycle changes without implementing any code inside the Activity or the Fragment.
- *
- * @see Lifecycle
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-public interface LifecycleOwner {
- /**
- * Returns the Lifecycle of the provider.
- *
- * @return The lifecycle of the provider.
- */
- @NonNull
- Lifecycle getLifecycle();
-}
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/Lifecycling.java b/lifecycle/common/src/main/java/android/arch/lifecycle/Lifecycling.java
deleted file mode 100644
index 7d6b37f..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/Lifecycling.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-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)
-public class Lifecycling {
-
- private static final int REFLECTIVE_CALLBACK = 1;
- private static final int GENERATED_CALLBACK = 2;
-
- 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 {
- return constructor.newInstance(object);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- } catch (InstantiationException e) {
- throw new RuntimeException(e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Nullable
- private static Constructor<? extends GeneratedAdapter> generatedConstructor(Class<?> klass) {
- try {
- 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);
- 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);
- }
- }
-
- 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/lifecycle/common/src/main/java/android/arch/lifecycle/MethodCallsLogger.java b/lifecycle/common/src/main/java/android/arch/lifecycle/MethodCallsLogger.java
deleted file mode 100644
index 031e43e..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/MethodCallsLogger.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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/lifecycle/common/src/main/java/android/arch/lifecycle/OnLifecycleEvent.java b/lifecycle/common/src/main/java/android/arch/lifecycle/OnLifecycleEvent.java
deleted file mode 100644
index 9a86b0c..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/OnLifecycleEvent.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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 java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@SuppressWarnings("unused")
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface OnLifecycleEvent {
- Lifecycle.Event value();
-}
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java
deleted file mode 100644
index f010ed8..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.arch.lifecycle.ClassesInfoCache.CallbackInfo;
-import android.arch.lifecycle.Lifecycle.Event;
-
-/**
- * An internal implementation of {@link GenericLifecycleObserver} that relies on reflection.
- */
-class ReflectiveGenericLifecycleObserver implements GenericLifecycleObserver {
- private final Object mWrapped;
- private final CallbackInfo mInfo;
-
- ReflectiveGenericLifecycleObserver(Object wrapped) {
- mWrapped = wrapped;
- mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
- }
-
- @Override
- public void onStateChanged(LifecycleOwner source, Event event) {
- mInfo.invokeCallbacks(source, event, mWrapped);
- }
-}
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/SingleGeneratedAdapterObserver.java b/lifecycle/common/src/main/java/android/arch/lifecycle/SingleGeneratedAdapterObserver.java
deleted file mode 100644
index d176a3a..0000000
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/SingleGeneratedAdapterObserver.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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/lifecycle/common/src/main/java/androidx/lifecycle/ClassesInfoCache.java b/lifecycle/common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
new file mode 100644
index 0000000..77d4916
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
@@ -0,0 +1,249 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.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 = getDeclaredMethods(klass);
+ 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;
+ }
+
+ private Method[] getDeclaredMethods(Class klass) {
+ try {
+ return klass.getDeclaredMethods();
+ } catch (NoClassDefFoundError e) {
+ throw new IllegalArgumentException("The observer class has some methods that use "
+ + "newer APIs which are not available in the current OS version. Lifecycles "
+ + "cannot access even other methods so you should make sure that your "
+ + "observer classes only access framework classes that are available "
+ + "in your min API level OR use lifecycle:compiler annotation processor.", e);
+ }
+ }
+
+ 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 : getDeclaredMethods(klass);
+ 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/lifecycle/common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.java b/lifecycle/common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.java
new file mode 100644
index 0000000..04e52fc
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/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 androidx.lifecycle;
+
+
+import androidx.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/lifecycle/common/src/main/java/androidx/lifecycle/FullLifecycleObserver.java b/lifecycle/common/src/main/java/androidx/lifecycle/FullLifecycleObserver.java
new file mode 100644
index 0000000..c959d03
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/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 androidx.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/lifecycle/common/src/main/java/androidx/lifecycle/FullLifecycleObserverAdapter.java b/lifecycle/common/src/main/java/androidx/lifecycle/FullLifecycleObserverAdapter.java
new file mode 100644
index 0000000..4047173
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/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 androidx.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/lifecycle/common/src/main/java/androidx/lifecycle/GeneratedAdapter.java b/lifecycle/common/src/main/java/androidx/lifecycle/GeneratedAdapter.java
new file mode 100644
index 0000000..a70d67b
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/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 androidx.lifecycle;
+
+import androidx.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/lifecycle/common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java b/lifecycle/common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java
new file mode 100644
index 0000000..ffdd2c0
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java
@@ -0,0 +1,35 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Internal class that can receive any lifecycle change and dispatch it to the receiver.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings({"WeakerAccess", "unused"})
+public interface GenericLifecycleObserver extends LifecycleObserver {
+ /**
+ * Called when a state transition event happens.
+ *
+ * @param source The source of the event
+ * @param event The event
+ */
+ void onStateChanged(LifecycleOwner source, Lifecycle.Event event);
+}
diff --git a/lifecycle/common/src/main/java/androidx/lifecycle/Lifecycle.java b/lifecycle/common/src/main/java/androidx/lifecycle/Lifecycle.java
new file mode 100644
index 0000000..9b6a5dc
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/Lifecycle.java
@@ -0,0 +1,202 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+
+/**
+ * Defines an object that has an Android Lifecycle. {@link androidx.fragment.app.Fragment Fragment}
+ * and {@link androidx.fragment.app.FragmentActivity FragmentActivity} classes implement
+ * {@link LifecycleOwner} interface which has the {@link LifecycleOwner#getLifecycle()
+ * getLifecycle} method to access the Lifecycle. You can also implement {@link LifecycleOwner}
+ * in your own classes.
+ * <p>
+ * {@link Event#ON_CREATE}, {@link Event#ON_START}, {@link Event#ON_RESUME} events in this class
+ * are dispatched <b>after</b> the {@link LifecycleOwner}'s related method returns.
+ * {@link Event#ON_PAUSE}, {@link Event#ON_STOP}, {@link Event#ON_DESTROY} events in this class
+ * are dispatched <b>before</b> the {@link LifecycleOwner}'s related method is called.
+ * For instance, {@link Event#ON_START} will be dispatched after
+ * {@link android.app.Activity#onStart onStart} returns, {@link Event#ON_STOP} will be dispatched
+ * before {@link android.app.Activity#onStop onStop} is called.
+ * This gives you certain guarantees on which state the owner is in.
+ * <p>
+ * If you use <b>Java 8 Language</b>, then observe events with {@link DefaultLifecycleObserver}.
+ * To include it you should add {@code "androidx.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)
+ * void onStopped() {}
+ * }
+ * </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
+ * of type {@link Event}.
+ * <pre>
+ * class TestObserver implements LifecycleObserver {
+ * {@literal @}OnLifecycleEvent(ON_CREATE)
+ * void onCreated(LifecycleOwner source) {}
+ * {@literal @}OnLifecycleEvent(ON_ANY)
+ * void onAny(LifecycleOwner source, Event event) {}
+ * }
+ * </pre>
+ * These additional parameters are provided to allow you to conveniently observe multiple providers
+ * and events without tracking them manually.
+ */
+public abstract class Lifecycle {
+ /**
+ * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
+ * state.
+ * <p>
+ * The given observer will be brought to the current state of the LifecycleOwner.
+ * For example, if the LifecycleOwner is in {@link State#STARTED} state, the given observer
+ * will receive {@link Event#ON_CREATE}, {@link Event#ON_START} events.
+ *
+ * @param observer The observer to notify.
+ */
+ @MainThread
+ public abstract void addObserver(@NonNull LifecycleObserver observer);
+
+ /**
+ * Removes the given observer from the observers list.
+ * <p>
+ * If this method is called while a state change is being dispatched,
+ * <ul>
+ * <li>If the given observer has not yet received that event, it will not receive it.
+ * <li>If the given observer has more than 1 method that observes the currently dispatched
+ * event and at least one of them received the event, all of them will receive the event and
+ * the removal will happen afterwards.
+ * </ul>
+ *
+ * @param observer The observer to be removed.
+ */
+ @MainThread
+ public abstract void removeObserver(@NonNull LifecycleObserver observer);
+
+ /**
+ * Returns the current state of the Lifecycle.
+ *
+ * @return The current state of the Lifecycle.
+ */
+ @MainThread
+ @NonNull
+ public abstract State getCurrentState();
+
+ @SuppressWarnings("WeakerAccess")
+ public enum Event {
+ /**
+ * Constant for onCreate event of the {@link LifecycleOwner}.
+ */
+ ON_CREATE,
+ /**
+ * Constant for onStart event of the {@link LifecycleOwner}.
+ */
+ ON_START,
+ /**
+ * Constant for onResume event of the {@link LifecycleOwner}.
+ */
+ ON_RESUME,
+ /**
+ * Constant for onPause event of the {@link LifecycleOwner}.
+ */
+ ON_PAUSE,
+ /**
+ * Constant for onStop event of the {@link LifecycleOwner}.
+ */
+ ON_STOP,
+ /**
+ * Constant for onDestroy event of the {@link LifecycleOwner}.
+ */
+ ON_DESTROY,
+ /**
+ * An {@link Event Event} constant that can be used to match all events.
+ */
+ ON_ANY
+ }
+
+ /**
+ * Lifecycle states. You can consider the states as the nodes in a graph and
+ * {@link Event}s as the edges between these nodes.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public enum State {
+ /**
+ * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch
+ * any more events. For instance, for an {@link android.app.Activity}, this state is reached
+ * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call.
+ */
+ DESTROYED,
+
+ /**
+ * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is
+ * the state when it is constructed but has not received
+ * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.
+ */
+ INITIALIZED,
+
+ /**
+ * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state
+ * is reached in two cases:
+ * <ul>
+ * <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;
+ * <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call.
+ * </ul>
+ */
+ CREATED,
+
+ /**
+ * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state
+ * is reached in two cases:
+ * <ul>
+ * <li>after {@link android.app.Activity#onStart() onStart} call;
+ * <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call.
+ * </ul>
+ */
+ STARTED,
+
+ /**
+ * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state
+ * is reached after {@link android.app.Activity#onResume() onResume} is called.
+ */
+ RESUMED;
+
+ /**
+ * Compares if this State is greater or equal to the given {@code state}.
+ *
+ * @param state State to compare with
+ * @return true if this State is greater or equal to the given {@code state}
+ */
+ public boolean isAtLeast(@NonNull State state) {
+ return compareTo(state) >= 0;
+ }
+ }
+}
diff --git a/lifecycle/common/src/main/java/androidx/lifecycle/LifecycleObserver.java b/lifecycle/common/src/main/java/androidx/lifecycle/LifecycleObserver.java
new file mode 100644
index 0000000..f02aad8
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/LifecycleObserver.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 androidx.lifecycle;
+
+/**
+ * Marks a class as a LifecycleObserver. It does not have any methods, instead, relies on
+ * {@link OnLifecycleEvent} annotated methods.
+ * <p>
+ * @see Lifecycle Lifecycle - for samples and usage patterns.
+ */
+@SuppressWarnings("WeakerAccess")
+public interface LifecycleObserver {
+
+}
diff --git a/lifecycle/common/src/main/java/androidx/lifecycle/LifecycleOwner.java b/lifecycle/common/src/main/java/androidx/lifecycle/LifecycleOwner.java
new file mode 100644
index 0000000..2ddffc2
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/LifecycleOwner.java
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A class that has an Android lifecycle. These events can be used by custom components to
+ * handle lifecycle changes without implementing any code inside the Activity or the Fragment.
+ *
+ * @see Lifecycle
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public interface LifecycleOwner {
+ /**
+ * Returns the Lifecycle of the provider.
+ *
+ * @return The lifecycle of the provider.
+ */
+ @NonNull
+ Lifecycle getLifecycle();
+}
diff --git a/lifecycle/common/src/main/java/androidx/lifecycle/Lifecycling.java b/lifecycle/common/src/main/java/androidx/lifecycle/Lifecycling.java
new file mode 100644
index 0000000..47c66c8
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/Lifecycling.java
@@ -0,0 +1,181 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+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)
+public class Lifecycling {
+
+ private static final int REFLECTIVE_CALLBACK = 1;
+ private static final int GENERATED_CALLBACK = 2;
+
+ 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 {
+ return constructor.newInstance(object);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Nullable
+ private static Constructor<? extends GeneratedAdapter> generatedConstructor(Class<?> klass) {
+ try {
+ 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);
+ 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);
+ }
+ }
+
+ 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/lifecycle/common/src/main/java/androidx/lifecycle/MethodCallsLogger.java b/lifecycle/common/src/main/java/androidx/lifecycle/MethodCallsLogger.java
new file mode 100644
index 0000000..0401a22
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/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 androidx.lifecycle;
+
+import androidx.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/lifecycle/common/src/main/java/androidx/lifecycle/OnLifecycleEvent.java b/lifecycle/common/src/main/java/androidx/lifecycle/OnLifecycleEvent.java
new file mode 100644
index 0000000..cfb72a0
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/OnLifecycleEvent.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 androidx.lifecycle;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@SuppressWarnings("unused")
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnLifecycleEvent {
+ Lifecycle.Event value();
+}
diff --git a/lifecycle/common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java b/lifecycle/common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
new file mode 100644
index 0000000..e94f02e
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.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 androidx.lifecycle;
+
+import androidx.lifecycle.ClassesInfoCache.CallbackInfo;
+import androidx.lifecycle.Lifecycle.Event;
+
+/**
+ * An internal implementation of {@link GenericLifecycleObserver} that relies on reflection.
+ */
+class ReflectiveGenericLifecycleObserver implements GenericLifecycleObserver {
+ private final Object mWrapped;
+ private final CallbackInfo mInfo;
+
+ ReflectiveGenericLifecycleObserver(Object wrapped) {
+ mWrapped = wrapped;
+ mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner source, Event event) {
+ mInfo.invokeCallbacks(source, event, mWrapped);
+ }
+}
diff --git a/lifecycle/common/src/main/java/androidx/lifecycle/SingleGeneratedAdapterObserver.java b/lifecycle/common/src/main/java/androidx/lifecycle/SingleGeneratedAdapterObserver.java
new file mode 100644
index 0000000..dc4d159
--- /dev/null
+++ b/lifecycle/common/src/main/java/androidx/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 androidx.lifecycle;
+
+import androidx.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/lifecycle/common/src/test/java/android/arch/lifecycle/FullLifecycleObserverTest.java b/lifecycle/common/src/test/java/android/arch/lifecycle/FullLifecycleObserverTest.java
deleted file mode 100644
index def6755..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/FullLifecycleObserverTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/LifecyclingTest.java b/lifecycle/common/src/test/java/android/arch/lifecycle/LifecyclingTest.java
deleted file mode 100644
index 70ce84c..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/LifecyclingTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/ReflectiveGenericLifecycleObserverTest.java b/lifecycle/common/src/test/java/android/arch/lifecycle/ReflectiveGenericLifecycleObserverTest.java
deleted file mode 100644
index 23aed28..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/ReflectiveGenericLifecycleObserverTest.java
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
- * 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_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.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-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.Matchers;
-
-@RunWith(JUnit4.class)
-public class ReflectiveGenericLifecycleObserverTest {
- 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 anyState() {
- AnyStateListener obj = mock(AnyStateListener.class);
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- when(mLifecycle.getCurrentState()).thenReturn(STARTED);
- observer.onStateChanged(mOwner, ON_CREATE);
- verify(obj).onAnyState(mOwner, ON_CREATE);
- reset(obj);
-
- observer.onStateChanged(mOwner, ON_START);
- verify(obj).onAnyState(mOwner, ON_START);
- reset(obj);
-
- observer.onStateChanged(mOwner, ON_RESUME);
- verify(obj).onAnyState(mOwner, ON_RESUME);
- reset(obj);
-
- observer.onStateChanged(mOwner, ON_PAUSE);
- verify(obj).onAnyState(mOwner, ON_PAUSE);
- reset(obj);
-
- observer.onStateChanged(mOwner, ON_STOP);
- verify(obj).onAnyState(mOwner, ON_STOP);
- reset(obj);
-
- observer.onStateChanged(mOwner, ON_DESTROY);
- verify(obj).onAnyState(mOwner, ON_DESTROY);
- reset(obj);
- }
-
- private static class AnyStateListener implements LifecycleObserver {
- @OnLifecycleEvent(ON_ANY)
- void onAnyState(LifecycleOwner owner, Lifecycle.Event event) {
-
- }
- }
-
- @Test
- public void singleMethod() {
- CreatedStateListener obj = mock(CreatedStateListener.class);
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- when(mLifecycle.getCurrentState()).thenReturn(CREATED);
- observer.onStateChanged(mOwner, ON_CREATE);
- verify(obj).onCreated();
- verify(obj).onCreated(mOwner);
- }
-
- private static class CreatedStateListener implements LifecycleObserver {
- @OnLifecycleEvent(ON_CREATE)
- void onCreated() {
-
- }
- @SuppressWarnings("UnusedParameters")
- @OnLifecycleEvent(ON_CREATE)
- void onCreated(LifecycleOwner provider) {
-
- }
- }
-
- @Test
- public void eachEvent() {
- AllMethodsListener obj = mock(AllMethodsListener.class);
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- when(mLifecycle.getCurrentState()).thenReturn(CREATED);
-
- observer.onStateChanged(mOwner, ON_CREATE);
- verify(obj).created();
- reset(obj);
-
- when(mLifecycle.getCurrentState()).thenReturn(STARTED);
- observer.onStateChanged(mOwner, ON_START);
- verify(obj).started();
- reset(obj);
-
- when(mLifecycle.getCurrentState()).thenReturn(RESUMED);
- observer.onStateChanged(mOwner, ON_RESUME);
- verify(obj).resumed();
- reset(obj);
-
- when(mLifecycle.getCurrentState()).thenReturn(STARTED);
- observer.onStateChanged(mOwner, ON_PAUSE);
- verify(obj).paused();
- reset(obj);
-
- when(mLifecycle.getCurrentState()).thenReturn(CREATED);
- observer.onStateChanged(mOwner, ON_STOP);
- verify(obj).stopped();
- reset(obj);
-
- when(mLifecycle.getCurrentState()).thenReturn(INITIALIZED);
- observer.onStateChanged(mOwner, ON_DESTROY);
- verify(obj).destroyed();
- reset(obj);
- }
-
-
- private static class AllMethodsListener implements LifecycleObserver {
- @OnLifecycleEvent(ON_CREATE)
- void created() {}
-
- @OnLifecycleEvent(ON_START)
- void started() {}
-
- @OnLifecycleEvent(ON_RESUME)
- void resumed() {}
-
- @OnLifecycleEvent(ON_PAUSE)
- void paused() {}
-
- @OnLifecycleEvent(ON_STOP)
- void stopped() {}
-
- @OnLifecycleEvent(ON_DESTROY)
- void destroyed() {
- }
- }
-
- @Test
- public void testFailingObserver() {
- class UnprecedentedError extends Error {
- }
-
- LifecycleObserver obj = new LifecycleObserver() {
- @OnLifecycleEvent(ON_START)
- void started() {
- throw new UnprecedentedError();
- }
- };
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- try {
- observer.onStateChanged(mOwner, ON_START);
- fail();
- } catch (Exception e) {
- assertThat("exception cause is wrong",
- e.getCause() instanceof UnprecedentedError);
- }
- }
-
- @Test
- public void testPrivateObserverMethods() {
- class ObserverWithPrivateMethod implements LifecycleObserver {
- boolean mCalled = false;
- @OnLifecycleEvent(ON_START)
- private void started() {
- mCalled = true;
- }
- }
-
- ObserverWithPrivateMethod obj = mock(ObserverWithPrivateMethod.class);
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- observer.onStateChanged(mOwner, ON_START);
- assertThat(obj.mCalled, is(true));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testWrongFirstParam1() {
- LifecycleObserver observer = new LifecycleObserver() {
- @OnLifecycleEvent(ON_START)
- private void started(Lifecycle.Event e) {
- }
- };
- new ReflectiveGenericLifecycleObserver(observer);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testWrongFirstParam2() {
- LifecycleObserver observer = new LifecycleObserver() {
- @OnLifecycleEvent(ON_ANY)
- private void started(Lifecycle l, Lifecycle.Event e) {
- }
- };
- new ReflectiveGenericLifecycleObserver(observer);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testWrongSecondParam() {
- LifecycleObserver observer = new LifecycleObserver() {
- @OnLifecycleEvent(ON_START)
- private void started(LifecycleOwner owner, Lifecycle l) {
- }
- };
- new ReflectiveGenericLifecycleObserver(observer);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testThreeParams() {
- LifecycleObserver observer = new LifecycleObserver() {
- @OnLifecycleEvent(ON_ANY)
- private void started(LifecycleOwner owner, Lifecycle.Event e, int i) {
- }
- };
- new ReflectiveGenericLifecycleObserver(observer);
- }
-
- static class BaseClass1 implements LifecycleObserver {
- @OnLifecycleEvent(ON_START)
- void foo(LifecycleOwner owner) {
- }
- }
-
- static class DerivedClass1 extends BaseClass1 {
- @Override
- @OnLifecycleEvent(ON_STOP)
- void foo(LifecycleOwner owner) {
- }
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testInvalidSuper1() {
- new ReflectiveGenericLifecycleObserver(new DerivedClass1());
- }
-
- static class BaseClass2 implements LifecycleObserver {
- @OnLifecycleEvent(ON_START)
- void foo(LifecycleOwner owner) {
- }
- }
-
- static class DerivedClass2 extends BaseClass1 {
- @OnLifecycleEvent(ON_STOP)
- void foo() {
- }
- }
-
- @Test
- public void testValidSuper1() {
- DerivedClass2 obj = mock(DerivedClass2.class);
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- observer.onStateChanged(mock(LifecycleOwner.class), ON_START);
- verify(obj).foo(Matchers.<LifecycleOwner>any());
- verify(obj, never()).foo();
- reset(obj);
- observer.onStateChanged(mock(LifecycleOwner.class), ON_STOP);
- verify(obj).foo();
- verify(obj, never()).foo(Matchers.<LifecycleOwner>any());
- }
-
- static class BaseClass3 implements LifecycleObserver {
- @OnLifecycleEvent(ON_START)
- void foo(LifecycleOwner owner) {
- }
- }
-
- interface Interface3 extends LifecycleObserver {
- @OnLifecycleEvent(ON_STOP)
- void foo(LifecycleOwner owner);
- }
-
- static class DerivedClass3 extends BaseClass3 implements Interface3 {
- @Override
- public void foo(LifecycleOwner owner) {
- }
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testInvalidSuper2() {
- new ReflectiveGenericLifecycleObserver(new DerivedClass3());
- }
-
- static class BaseClass4 implements LifecycleObserver {
- @OnLifecycleEvent(ON_START)
- void foo(LifecycleOwner owner) {
- }
- }
-
- interface Interface4 extends LifecycleObserver {
- @OnLifecycleEvent(ON_START)
- void foo(LifecycleOwner owner);
- }
-
- static class DerivedClass4 extends BaseClass4 implements Interface4 {
- @Override
- @OnLifecycleEvent(ON_START)
- public void foo(LifecycleOwner owner) {
- }
-
- @OnLifecycleEvent(ON_START)
- public void foo() {
- }
- }
-
- @Test
- public void testValidSuper2() {
- DerivedClass4 obj = mock(DerivedClass4.class);
- ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
- observer.onStateChanged(mock(LifecycleOwner.class), ON_START);
- verify(obj).foo(Matchers.<LifecycleOwner>any());
- verify(obj).foo();
- }
-
- interface InterfaceStart extends LifecycleObserver {
- @OnLifecycleEvent(ON_START)
- void foo(LifecycleOwner owner);
- }
-
- interface InterfaceStop extends LifecycleObserver {
- @OnLifecycleEvent(ON_STOP)
- void foo(LifecycleOwner owner);
- }
-
- static class DerivedClass5 implements InterfaceStart, InterfaceStop {
- @Override
- public void foo(LifecycleOwner owner) {
- }
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testInvalidSuper3() {
- new ReflectiveGenericLifecycleObserver(new DerivedClass5());
- }
-}
diff --git a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Base.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Base.java
deleted file mode 100644
index 08919d4..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Base.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Base_LifecycleAdapter.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Base_LifecycleAdapter.java
deleted file mode 100644
index 4218b96..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Base_LifecycleAdapter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedSequence1.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedSequence1.java
deleted file mode 100644
index 9db37f1..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedSequence1.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedSequence2.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedSequence2.java
deleted file mode 100644
index f2ef943..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedSequence2.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithNewMethods.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithNewMethods.java
deleted file mode 100644
index b1eaef0..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithNewMethods.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java
deleted file mode 100644
index cb1afb8..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java
deleted file mode 100644
index 40c7c9a..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface1.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface1.java
deleted file mode 100644
index e193de9..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface1.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java
deleted file mode 100644
index c597b1c..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface2.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface2.java
deleted file mode 100644
index 1056fcb..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface2.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java
deleted file mode 100644
index b05b41a..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl1.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl1.java
deleted file mode 100644
index 2f03393..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl1.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl2.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl2.java
deleted file mode 100644
index eef8ce4..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl2.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl3.java b/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl3.java
deleted file mode 100644
index 8f31808..0000000
--- a/lifecycle/common/src/test/java/android/arch/lifecycle/observers/InterfaceImpl3.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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/lifecycle/common/src/test/java/androidx/lifecycle/FullLifecycleObserverTest.java b/lifecycle/common/src/test/java/androidx/lifecycle/FullLifecycleObserverTest.java
new file mode 100644
index 0000000..79dd8f4
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
+import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
+import static androidx.lifecycle.Lifecycle.State.RESUMED;
+import static androidx.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/lifecycle/common/src/test/java/androidx/lifecycle/LifecyclingTest.java b/lifecycle/common/src/test/java/androidx/lifecycle/LifecyclingTest.java
new file mode 100644
index 0000000..9cc251e
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import androidx.lifecycle.observers.DerivedSequence1;
+import androidx.lifecycle.observers.DerivedSequence2;
+import androidx.lifecycle.observers.DerivedWithNewMethods;
+import androidx.lifecycle.observers.DerivedWithNoNewMethods;
+import androidx.lifecycle.observers.DerivedWithOverridenMethodsWithLfAnnotation;
+import androidx.lifecycle.observers.InterfaceImpl1;
+import androidx.lifecycle.observers.InterfaceImpl2;
+import androidx.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/lifecycle/common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.java b/lifecycle/common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.java
new file mode 100644
index 0000000..4966500
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
+import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
+import static androidx.lifecycle.Lifecycle.State.RESUMED;
+import static androidx.lifecycle.Lifecycle.State.STARTED;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+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.Matchers;
+
+@RunWith(JUnit4.class)
+public class ReflectiveGenericLifecycleObserverTest {
+ 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 anyState() {
+ AnyStateListener obj = mock(AnyStateListener.class);
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ when(mLifecycle.getCurrentState()).thenReturn(STARTED);
+ observer.onStateChanged(mOwner, ON_CREATE);
+ verify(obj).onAnyState(mOwner, ON_CREATE);
+ reset(obj);
+
+ observer.onStateChanged(mOwner, ON_START);
+ verify(obj).onAnyState(mOwner, ON_START);
+ reset(obj);
+
+ observer.onStateChanged(mOwner, ON_RESUME);
+ verify(obj).onAnyState(mOwner, ON_RESUME);
+ reset(obj);
+
+ observer.onStateChanged(mOwner, ON_PAUSE);
+ verify(obj).onAnyState(mOwner, ON_PAUSE);
+ reset(obj);
+
+ observer.onStateChanged(mOwner, ON_STOP);
+ verify(obj).onAnyState(mOwner, ON_STOP);
+ reset(obj);
+
+ observer.onStateChanged(mOwner, ON_DESTROY);
+ verify(obj).onAnyState(mOwner, ON_DESTROY);
+ reset(obj);
+ }
+
+ private static class AnyStateListener implements LifecycleObserver {
+ @OnLifecycleEvent(ON_ANY)
+ void onAnyState(LifecycleOwner owner, Lifecycle.Event event) {
+
+ }
+ }
+
+ @Test
+ public void singleMethod() {
+ CreatedStateListener obj = mock(CreatedStateListener.class);
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ when(mLifecycle.getCurrentState()).thenReturn(CREATED);
+ observer.onStateChanged(mOwner, ON_CREATE);
+ verify(obj).onCreated();
+ verify(obj).onCreated(mOwner);
+ }
+
+ private static class CreatedStateListener implements LifecycleObserver {
+ @OnLifecycleEvent(ON_CREATE)
+ void onCreated() {
+
+ }
+ @SuppressWarnings("UnusedParameters")
+ @OnLifecycleEvent(ON_CREATE)
+ void onCreated(LifecycleOwner provider) {
+
+ }
+ }
+
+ @Test
+ public void eachEvent() {
+ AllMethodsListener obj = mock(AllMethodsListener.class);
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ when(mLifecycle.getCurrentState()).thenReturn(CREATED);
+
+ observer.onStateChanged(mOwner, ON_CREATE);
+ verify(obj).created();
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(STARTED);
+ observer.onStateChanged(mOwner, ON_START);
+ verify(obj).started();
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(RESUMED);
+ observer.onStateChanged(mOwner, ON_RESUME);
+ verify(obj).resumed();
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(STARTED);
+ observer.onStateChanged(mOwner, ON_PAUSE);
+ verify(obj).paused();
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(CREATED);
+ observer.onStateChanged(mOwner, ON_STOP);
+ verify(obj).stopped();
+ reset(obj);
+
+ when(mLifecycle.getCurrentState()).thenReturn(INITIALIZED);
+ observer.onStateChanged(mOwner, ON_DESTROY);
+ verify(obj).destroyed();
+ reset(obj);
+ }
+
+
+ private static class AllMethodsListener implements LifecycleObserver {
+ @OnLifecycleEvent(ON_CREATE)
+ void created() {}
+
+ @OnLifecycleEvent(ON_START)
+ void started() {}
+
+ @OnLifecycleEvent(ON_RESUME)
+ void resumed() {}
+
+ @OnLifecycleEvent(ON_PAUSE)
+ void paused() {}
+
+ @OnLifecycleEvent(ON_STOP)
+ void stopped() {}
+
+ @OnLifecycleEvent(ON_DESTROY)
+ void destroyed() {
+ }
+ }
+
+ @Test
+ public void testFailingObserver() {
+ class UnprecedentedError extends Error {
+ }
+
+ LifecycleObserver obj = new LifecycleObserver() {
+ @OnLifecycleEvent(ON_START)
+ void started() {
+ throw new UnprecedentedError();
+ }
+ };
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ try {
+ observer.onStateChanged(mOwner, ON_START);
+ fail();
+ } catch (Exception e) {
+ assertThat("exception cause is wrong",
+ e.getCause() instanceof UnprecedentedError);
+ }
+ }
+
+ @Test
+ public void testPrivateObserverMethods() {
+ class ObserverWithPrivateMethod implements LifecycleObserver {
+ boolean mCalled = false;
+ @OnLifecycleEvent(ON_START)
+ private void started() {
+ mCalled = true;
+ }
+ }
+
+ ObserverWithPrivateMethod obj = mock(ObserverWithPrivateMethod.class);
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ observer.onStateChanged(mOwner, ON_START);
+ assertThat(obj.mCalled, is(true));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWrongFirstParam1() {
+ LifecycleObserver observer = new LifecycleObserver() {
+ @OnLifecycleEvent(ON_START)
+ private void started(Lifecycle.Event e) {
+ }
+ };
+ new ReflectiveGenericLifecycleObserver(observer);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWrongFirstParam2() {
+ LifecycleObserver observer = new LifecycleObserver() {
+ @OnLifecycleEvent(ON_ANY)
+ private void started(Lifecycle l, Lifecycle.Event e) {
+ }
+ };
+ new ReflectiveGenericLifecycleObserver(observer);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testWrongSecondParam() {
+ LifecycleObserver observer = new LifecycleObserver() {
+ @OnLifecycleEvent(ON_START)
+ private void started(LifecycleOwner owner, Lifecycle l) {
+ }
+ };
+ new ReflectiveGenericLifecycleObserver(observer);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThreeParams() {
+ LifecycleObserver observer = new LifecycleObserver() {
+ @OnLifecycleEvent(ON_ANY)
+ private void started(LifecycleOwner owner, Lifecycle.Event e, int i) {
+ }
+ };
+ new ReflectiveGenericLifecycleObserver(observer);
+ }
+
+ static class BaseClass1 implements LifecycleObserver {
+ @OnLifecycleEvent(ON_START)
+ void foo(LifecycleOwner owner) {
+ }
+ }
+
+ static class DerivedClass1 extends BaseClass1 {
+ @Override
+ @OnLifecycleEvent(ON_STOP)
+ void foo(LifecycleOwner owner) {
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidSuper1() {
+ new ReflectiveGenericLifecycleObserver(new DerivedClass1());
+ }
+
+ static class BaseClass2 implements LifecycleObserver {
+ @OnLifecycleEvent(ON_START)
+ void foo(LifecycleOwner owner) {
+ }
+ }
+
+ static class DerivedClass2 extends BaseClass1 {
+ @OnLifecycleEvent(ON_STOP)
+ void foo() {
+ }
+ }
+
+ @Test
+ public void testValidSuper1() {
+ DerivedClass2 obj = mock(DerivedClass2.class);
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ observer.onStateChanged(mock(LifecycleOwner.class), ON_START);
+ verify(obj).foo(Matchers.<LifecycleOwner>any());
+ verify(obj, never()).foo();
+ reset(obj);
+ observer.onStateChanged(mock(LifecycleOwner.class), ON_STOP);
+ verify(obj).foo();
+ verify(obj, never()).foo(Matchers.<LifecycleOwner>any());
+ }
+
+ static class BaseClass3 implements LifecycleObserver {
+ @OnLifecycleEvent(ON_START)
+ void foo(LifecycleOwner owner) {
+ }
+ }
+
+ interface Interface3 extends LifecycleObserver {
+ @OnLifecycleEvent(ON_STOP)
+ void foo(LifecycleOwner owner);
+ }
+
+ static class DerivedClass3 extends BaseClass3 implements Interface3 {
+ @Override
+ public void foo(LifecycleOwner owner) {
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidSuper2() {
+ new ReflectiveGenericLifecycleObserver(new DerivedClass3());
+ }
+
+ static class BaseClass4 implements LifecycleObserver {
+ @OnLifecycleEvent(ON_START)
+ void foo(LifecycleOwner owner) {
+ }
+ }
+
+ interface Interface4 extends LifecycleObserver {
+ @OnLifecycleEvent(ON_START)
+ void foo(LifecycleOwner owner);
+ }
+
+ static class DerivedClass4 extends BaseClass4 implements Interface4 {
+ @Override
+ @OnLifecycleEvent(ON_START)
+ public void foo(LifecycleOwner owner) {
+ }
+
+ @OnLifecycleEvent(ON_START)
+ public void foo() {
+ }
+ }
+
+ @Test
+ public void testValidSuper2() {
+ DerivedClass4 obj = mock(DerivedClass4.class);
+ ReflectiveGenericLifecycleObserver observer = new ReflectiveGenericLifecycleObserver(obj);
+ observer.onStateChanged(mock(LifecycleOwner.class), ON_START);
+ verify(obj).foo(Matchers.<LifecycleOwner>any());
+ verify(obj).foo();
+ }
+
+ interface InterfaceStart extends LifecycleObserver {
+ @OnLifecycleEvent(ON_START)
+ void foo(LifecycleOwner owner);
+ }
+
+ interface InterfaceStop extends LifecycleObserver {
+ @OnLifecycleEvent(ON_STOP)
+ void foo(LifecycleOwner owner);
+ }
+
+ static class DerivedClass5 implements InterfaceStart, InterfaceStop {
+ @Override
+ public void foo(LifecycleOwner owner) {
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidSuper3() {
+ new ReflectiveGenericLifecycleObserver(new DerivedClass5());
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/Base.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/Base.java
new file mode 100644
index 0000000..39649eb
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+
+public class Base implements LifecycleObserver {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ public void onCreate() {
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.java
new file mode 100644
index 0000000..155825c
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.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/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedSequence1.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedSequence1.java
new file mode 100644
index 0000000..ae9c85f
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+public class DerivedSequence1 extends Base {
+
+ public void something() {
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedSequence2.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedSequence2.java
new file mode 100644
index 0000000..38b66c5
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.OnLifecycleEvent;
+
+public class DerivedSequence2 extends DerivedSequence1 {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ void onStop() {
+
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedWithNewMethods.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedWithNewMethods.java
new file mode 100644
index 0000000..dc5dc13
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.OnLifecycleEvent;
+
+public class DerivedWithNewMethods extends Base {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ void onStop() {
+
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.java
new file mode 100644
index 0000000..d8bfeea
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+public class DerivedWithNoNewMethods extends Base {
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java
new file mode 100644
index 0000000..d1cec8b
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.OnLifecycleEvent;
+
+public class DerivedWithOverridenMethodsWithLfAnnotation extends Base {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface1.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface1.java
new file mode 100644
index 0000000..f6f4960
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+
+public interface Interface1 extends LifecycleObserver {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ void onCreate();
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.java
new file mode 100644
index 0000000..5dc2fd2
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.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/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface2.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface2.java
new file mode 100644
index 0000000..080d43d
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+
+public interface Interface2 extends LifecycleObserver {
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ void onCreate();
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ void onDestroy();
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.java
new file mode 100644
index 0000000..9c02c43
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.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/lifecycle/common/src/test/java/androidx/lifecycle/observers/InterfaceImpl1.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/InterfaceImpl1.java
new file mode 100644
index 0000000..0b07605
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+public class InterfaceImpl1 implements Interface1 {
+ @Override
+ public void onCreate() {
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/InterfaceImpl2.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/InterfaceImpl2.java
new file mode 100644
index 0000000..2f5d9d9
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+public class InterfaceImpl2 implements Interface1, Interface2 {
+ @Override
+ public void onCreate() {
+ }
+
+ @Override
+ public void onDestroy() {
+
+ }
+}
diff --git a/lifecycle/common/src/test/java/androidx/lifecycle/observers/InterfaceImpl3.java b/lifecycle/common/src/test/java/androidx/lifecycle/observers/InterfaceImpl3.java
new file mode 100644
index 0000000..e6d8a63
--- /dev/null
+++ b/lifecycle/common/src/test/java/androidx/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 androidx.lifecycle.observers;
+
+public class InterfaceImpl3 extends Base implements Interface1 {
+ @Override
+ public void onCreate() {
+ }
+}
diff --git a/lifecycle/compiler/build.gradle b/lifecycle/compiler/build.gradle
index fec3d99..90df541 100644
--- a/lifecycle/compiler/build.gradle
+++ b/lifecycle/compiler/build.gradle
@@ -19,7 +19,7 @@
}
dependencies {
- compile(project(":lifecycle:common"))
+ compile(project(":lifecycle:lifecycle-common"))
compile(KOTLIN_STDLIB)
compile(AUTO_COMMON)
compile(JAVAPOET)
@@ -28,7 +28,7 @@
testCompile files(org.gradle.internal.jvm.Jvm.current().getToolsJar())
}
-// we actually need to compile :lifecycle:common, but compileJava is easier
+// we actually need to compile :lifecycle:lifecycle-common, but compileJava is easier
task compileTestLibrarySource(type: JavaCompile, dependsOn: compileJava) {
source "src/tests/test-data/lib/src"
classpath = project.compileJava.classpath
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/ErrorMessages.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/ErrorMessages.kt
deleted file mode 100644
index 2f300e7..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/ErrorMessages.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.arch.lifecycle.model.EventMethod
-import javax.lang.model.element.TypeElement
-
-object ErrorMessages {
- const val TOO_MANY_ARGS = "callback method cannot have more than 2 parameters"
- const val TOO_MANY_ARGS_NOT_ON_ANY = "only callback annotated with ON_ANY " +
- "can have 2 parameters"
- const val INVALID_SECOND_ARGUMENT = "2nd argument of a callback method" +
- " must be Lifecycle.Event and represent the current event"
- const val INVALID_FIRST_ARGUMENT = "1st argument of a callback method must be " +
- "a LifecycleOwner which represents the source of the event"
- const val INVALID_METHOD_MODIFIER = "method marked with OnLifecycleEvent annotation can " +
- "not be private"
- const val INVALID_CLASS_MODIFIER = "class containing OnLifecycleEvent methods can not be " +
- "private"
- const val INVALID_STATE_OVERRIDE_METHOD = "overridden method must handle the same " +
- "onState changes as original method"
- const val INVALID_ENCLOSING_ELEMENT =
- "Parent of OnLifecycleEvent should be a class or interface"
- const val INVALID_ANNOTATED_ELEMENT = "OnLifecycleEvent can only be added to methods"
-
- fun failedToGenerateAdapter(type: TypeElement, failureReason: EventMethod) =
- """
- Failed to generate an Adapter for $type, because it needs to be able to access to
- package private method ${failureReason.method.name()} from ${failureReason.type}
- """.trim()
-}
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt
deleted file mode 100644
index f57d2dd..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/LifecycleProcessor.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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 javax.annotation.processing.AbstractProcessor
-import javax.annotation.processing.RoundEnvironment
-import javax.annotation.processing.SupportedAnnotationTypes
-import javax.lang.model.SourceVersion
-import javax.lang.model.element.TypeElement
-
-@SupportedAnnotationTypes("android.arch.lifecycle.OnLifecycleEvent")
-class LifecycleProcessor : AbstractProcessor() {
- override fun process(annotations: MutableSet<out TypeElement>,
- roundEnv: RoundEnvironment): Boolean {
- val input = collectAndVerifyInput(processingEnv, roundEnv)
- writeModels(transformToOutput(processingEnv, input), processingEnv)
- return true
- }
-
- override fun getSupportedSourceVersion(): SourceVersion {
- return SourceVersion.latest()
- }
-}
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/elements_ext.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/elements_ext.kt
deleted file mode 100644
index 08b6ff2..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/elements_ext.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 com.google.auto.common.MoreElements
-import javax.lang.model.element.Element
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.PackageElement
-import javax.lang.model.element.TypeElement
-import javax.lang.model.util.ElementFilter
-
-fun Element.getPackage(): PackageElement = MoreElements.getPackage(this)
-
-fun Element.getPackageQName() = getPackage().qualifiedName.toString()
-
-fun ExecutableElement.name() = simpleName.toString()
-
-fun ExecutableElement.isPackagePrivate() = !modifiers.any {
- it == Modifier.PUBLIC || it == Modifier.PROTECTED || it == Modifier.PRIVATE
-}
-
-fun ExecutableElement.isProtected() = modifiers.contains(Modifier.PROTECTED)
-
-fun TypeElement.methods(): List<ExecutableElement> = ElementFilter.methodsIn(enclosedElements)
-
-private const val SYNTHETIC = "__synthetic_"
-
-fun syntheticName(method: ExecutableElement) = "$SYNTHETIC${method.simpleName}"
-
-fun isSyntheticMethod(method: ExecutableElement) = method.name().startsWith(SYNTHETIC)
-
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/input_collector.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/input_collector.kt
deleted file mode 100644
index 843ac4e..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/input_collector.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * 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.arch.lifecycle.model.EventMethod
-import android.arch.lifecycle.model.InputModel
-import android.arch.lifecycle.model.LifecycleObserverInfo
-import android.arch.lifecycle.model.getAdapterName
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import javax.annotation.processing.ProcessingEnvironment
-import javax.annotation.processing.RoundEnvironment
-import javax.lang.model.element.Element
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.lang.model.element.VariableElement
-import javax.lang.model.type.TypeMirror
-import javax.lang.model.util.Elements
-import javax.lang.model.util.Types
-import javax.tools.Diagnostic
-
-fun collectAndVerifyInput(processingEnv: ProcessingEnvironment,
- roundEnv: RoundEnvironment): InputModel {
- val validator = Validator(processingEnv)
- val worldCollector = ObserversCollector(processingEnv)
- val roots = roundEnv.getElementsAnnotatedWith(OnLifecycleEvent::class.java).map { elem ->
- if (elem.kind != ElementKind.METHOD) {
- validator.printErrorMessage(ErrorMessages.INVALID_ANNOTATED_ELEMENT, elem)
- null
- } else {
- val enclosingElement = elem.enclosingElement
- if (validator.validateClass(enclosingElement)) {
- MoreElements.asType(enclosingElement)
- } else {
- null
- }
- }
- }.filterNotNull().toSet()
- roots.forEach { worldCollector.collect(it) }
- val observersInfo = worldCollector.observers
- val generatedAdapters = worldCollector.observers.keys
- .mapNotNull { type ->
- worldCollector.generatedAdapterInfoFor(type)?.let { type to it }
- }.toMap()
- return InputModel(roots, observersInfo, generatedAdapters)
-}
-
-class ObserversCollector(processingEnv: ProcessingEnvironment) {
- val typeUtils: Types = processingEnv.typeUtils
- val elementUtils: Elements = processingEnv.elementUtils
- val lifecycleObserverTypeMirror: TypeMirror =
- elementUtils.getTypeElement(LifecycleObserver::class.java.canonicalName).asType()
- val validator = Validator(processingEnv)
- val observers: MutableMap<TypeElement, LifecycleObserverInfo> = mutableMapOf()
-
- fun collect(type: TypeElement): LifecycleObserverInfo? {
- if (type in observers) {
- return observers[type]
- }
- val parents = (listOf(type.superclass) + type.interfaces)
- .filter { typeUtils.isAssignable(it, lifecycleObserverTypeMirror) }
- .filterNot { typeUtils.isSameType(it, lifecycleObserverTypeMirror) }
- .map { collect(MoreTypes.asTypeElement(it)) }
- .filterNotNull()
- val info = createObserverInfo(type, parents)
- if (info != null) {
- observers[type] = info
- }
- return info
- }
-
- fun generatedAdapterInfoFor(type: TypeElement): List<ExecutableElement>? {
- val packageName = if (type.getPackageQName().isEmpty()) "" else "${type.getPackageQName()}."
- val adapterType = elementUtils.getTypeElement(packageName + getAdapterName(type))
- return adapterType?.methods()
- ?.filter { executable -> isSyntheticMethod(executable) }
- }
-
- private fun createObserverInfo(typeElement: TypeElement,
- parents: List<LifecycleObserverInfo>): LifecycleObserverInfo? {
- if (!validator.validateClass(typeElement)) {
- return null
- }
- val methods = typeElement.methods().filter { executable ->
- MoreElements.isAnnotationPresent(executable, OnLifecycleEvent::class.java)
- }.map { executable ->
- val onState = executable.getAnnotation(OnLifecycleEvent::class.java)
- if (validator.validateMethod(executable, onState.value)) {
- EventMethod(executable, onState, typeElement)
- } else {
- null
- }
- }.filterNotNull()
- return LifecycleObserverInfo(typeElement, methods, parents)
- }
-}
-
-class Validator(val processingEnv: ProcessingEnvironment) {
-
- fun printErrorMessage(msg: CharSequence, elem: Element) {
- processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, elem)
- }
-
- fun validateParam(param: VariableElement,
- expectedType: Class<*>, errorMsg: String): Boolean {
- if (!MoreTypes.isTypeOf(expectedType, param.asType())) {
- printErrorMessage(errorMsg, param)
- return false
- }
- return true
- }
-
- fun validateMethod(method: ExecutableElement, event: Lifecycle.Event): Boolean {
- if (Modifier.PRIVATE in method.modifiers) {
- printErrorMessage(ErrorMessages.INVALID_METHOD_MODIFIER, method)
- return false
- }
- val params = method.parameters
- if ((params.size > 2)) {
- printErrorMessage(ErrorMessages.TOO_MANY_ARGS, method)
- return false
- }
-
- if (params.size == 2 && event != Lifecycle.Event.ON_ANY) {
- printErrorMessage(ErrorMessages.TOO_MANY_ARGS_NOT_ON_ANY, method)
- return false
- }
-
- if (params.size == 2 && !validateParam(params[1], Lifecycle.Event::class.java,
- ErrorMessages.INVALID_SECOND_ARGUMENT)) {
- return false
- }
-
- if (params.size > 0) {
- return validateParam(params[0], LifecycleOwner::class.java,
- ErrorMessages.INVALID_FIRST_ARGUMENT)
- }
- return true
- }
-
- fun validateClass(classElement: Element): Boolean {
- if (!MoreElements.isType(classElement)) {
- printErrorMessage(ErrorMessages.INVALID_ENCLOSING_ELEMENT, classElement)
- return false
- }
- if (Modifier.PRIVATE in classElement.modifiers) {
- printErrorMessage(ErrorMessages.INVALID_CLASS_MODIFIER, classElement)
- return false
- }
- return true
- }
-}
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/AdapterClass.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/AdapterClass.kt
deleted file mode 100644
index bd7759a..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/AdapterClass.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.model
-
-import android.arch.lifecycle.Lifecycling
-import android.arch.lifecycle.getPackage
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.TypeElement
-
-data class AdapterClass(val type: TypeElement,
- val calls: List<EventMethodCall>,
- val syntheticMethods: Set<ExecutableElement>)
-
-fun getAdapterName(type: TypeElement): String {
- val packageElement = type.getPackage()
- val qName = type.qualifiedName.toString()
- val partialName = if (packageElement.isUnnamed) qName else qName.substring(
- packageElement.qualifiedName.toString().length + 1)
- return Lifecycling.getAdapterName(partialName)
-}
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/EventMethod.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/EventMethod.kt
deleted file mode 100644
index a3d4712..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/EventMethod.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.model
-
-import android.arch.lifecycle.OnLifecycleEvent
-import android.arch.lifecycle.getPackageQName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.TypeElement
-
-data class EventMethod(val method: ExecutableElement,
- val onLifecycleEvent: OnLifecycleEvent,
- val type: TypeElement) {
-
- fun packageName() = type.getPackageQName()
-}
-
-data class EventMethodCall(val method: EventMethod, val syntheticAccess: TypeElement? = null)
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/InputModel.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/InputModel.kt
deleted file mode 100644
index c1e5d2b..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/InputModel.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.model
-
-import android.arch.lifecycle.name
-import android.arch.lifecycle.syntheticName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.TypeElement
-
-data class InputModel(
- // all java files with lifecycle annotations excluding classes from classpath
- private val rootTypes: Set<TypeElement>,
- // info about all lifecycle observers including classes from classpath
- val observersInfo: Map<TypeElement, LifecycleObserverInfo>,
- // info about generated adapters from class path
- val generatedAdapters: Map<TypeElement, List<ExecutableElement>>) {
-
- /**
- * Root class is class defined in currently processed module, not in classpath
- */
- fun isRootType(type: TypeElement) = type in rootTypes
-
- fun hasSyntheticAccessorFor(eventMethod: EventMethod): Boolean {
- val syntheticMethods = generatedAdapters[eventMethod.type] ?: return false
- return syntheticMethods.any { executable ->
- executable.name() == syntheticName(eventMethod.method)
- // same number + receiver object
- && (eventMethod.method.parameters.size + 1) == executable.parameters.size
- }
- }
-}
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/LifecycleObserverInfo.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/LifecycleObserverInfo.kt
deleted file mode 100644
index 1477cbf..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/model/LifecycleObserverInfo.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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.model
-
-import javax.lang.model.element.TypeElement
-
-data class LifecycleObserverInfo(
- val type: TypeElement,
- val methods: List<EventMethod>,
- val parents: List<LifecycleObserverInfo> = listOf())
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/transformation.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/transformation.kt
deleted file mode 100644
index 4b93472..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/transformation.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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.arch.lifecycle.model.AdapterClass
-import android.arch.lifecycle.model.EventMethod
-import android.arch.lifecycle.model.EventMethodCall
-import android.arch.lifecycle.model.InputModel
-import android.arch.lifecycle.model.LifecycleObserverInfo
-import com.google.common.collect.HashMultimap
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.TypeElement
-import javax.tools.Diagnostic
-
-private fun mergeAndVerifyMethods(processingEnv: ProcessingEnvironment,
- type: TypeElement,
- classMethods: List<EventMethod>,
- parentMethods: List<EventMethod>): List<EventMethod> {
- // need to update parent methods like that because:
- // 1. visibility can be expanded
- // 2. we want to preserve order
- val updatedParentMethods = parentMethods.map { parentMethod ->
- val overrideMethod = classMethods.find { (method) ->
- processingEnv.elementUtils.overrides(method, parentMethod.method, type)
- }
- if (overrideMethod != null) {
- if (overrideMethod.onLifecycleEvent != parentMethod.onLifecycleEvent) {
- processingEnv.messager.printMessage(Diagnostic.Kind.ERROR,
- ErrorMessages.INVALID_STATE_OVERRIDE_METHOD, overrideMethod.method)
- }
- overrideMethod
- } else {
- parentMethod
- }
- }
- return updatedParentMethods + classMethods.filterNot { updatedParentMethods.contains(it) }
-}
-
-fun flattenObservers(processingEnv: ProcessingEnvironment,
- world: Map<TypeElement, LifecycleObserverInfo>): List<LifecycleObserverInfo> {
- val flattened: MutableMap<LifecycleObserverInfo, LifecycleObserverInfo> = mutableMapOf()
-
- fun traverse(observer: LifecycleObserverInfo) {
- if (observer in flattened) {
- return
- }
- if (observer.parents.isEmpty()) {
- flattened[observer] = observer
- return
- }
- observer.parents.forEach(::traverse)
- val methods = observer.parents
- .map(flattened::get)
- .fold(emptyList<EventMethod>()) { list, parentObserver ->
- mergeAndVerifyMethods(processingEnv, observer.type,
- parentObserver!!.methods, list)
- }
-
- flattened[observer] = LifecycleObserverInfo(observer.type,
- mergeAndVerifyMethods(processingEnv, observer.type, observer.methods, methods))
- }
-
- world.values.forEach(::traverse)
- return flattened.values.toList()
-}
-
-private fun needsSyntheticAccess(type: TypeElement, eventMethod: EventMethod): Boolean {
- val executable = eventMethod.method
- return type.getPackageQName() != eventMethod.packageName()
- && (executable.isPackagePrivate() || executable.isProtected())
-}
-
-private fun validateMethod(processingEnv: ProcessingEnvironment,
- world: InputModel, type: TypeElement,
- eventMethod: EventMethod): Boolean {
- if (!needsSyntheticAccess(type, eventMethod)) {
- // no synthetic calls - no problems
- return true
- }
-
- if (world.isRootType(eventMethod.type)) {
- // we will generate adapters for them, so we can generate all accessors
- return true
- }
-
- if (world.hasSyntheticAccessorFor(eventMethod)) {
- // previously generated adapter already has synthetic
- return true
- }
-
- processingEnv.messager.printMessage(Diagnostic.Kind.WARNING,
- ErrorMessages.failedToGenerateAdapter(type, eventMethod), type)
- return false
-}
-
-fun transformToOutput(processingEnv: ProcessingEnvironment,
- world: InputModel): List<AdapterClass> {
- val flatObservers = flattenObservers(processingEnv, world.observersInfo)
- val syntheticMethods = HashMultimap.create<TypeElement, EventMethodCall>()
- val adapterCalls = flatObservers
- // filter out everything that arrived from jars
- .filter { (type) -> world.isRootType(type) }
- // filter out if it needs SYNTHETIC access and we can't generate adapter for it
- .filter { (type, methods) ->
- methods.all { eventMethod ->
- validateMethod(processingEnv, world, type, eventMethod)
- }
- }
- .map { (type, methods) ->
- val calls = methods.map { eventMethod ->
- if (needsSyntheticAccess(type, eventMethod)) {
- EventMethodCall(eventMethod, eventMethod.type)
- } else {
- EventMethodCall(eventMethod)
- }
- }
- calls.filter { it.syntheticAccess != null }.forEach { eventMethod ->
- syntheticMethods.put(eventMethod.method.type, eventMethod)
- }
- type to calls
- }.toMap()
-
- return adapterCalls
- .map { (type, calls) ->
- val methods = syntheticMethods.get(type) ?: emptySet()
- val synthetic = methods.map { eventMethod -> eventMethod!!.method.method }.toSet()
- AdapterClass(type, calls, synthetic)
- }
-}
diff --git a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/writer.kt b/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/writer.kt
deleted file mode 100644
index 5811c61..0000000
--- a/lifecycle/compiler/src/main/kotlin/android/arch/lifecycle/writer.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.arch.lifecycle.model.AdapterClass
-import android.arch.lifecycle.model.EventMethodCall
-import android.arch.lifecycle.model.getAdapterName
-import com.squareup.javapoet.AnnotationSpec
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.tools.StandardLocation
-
-fun writeModels(infos: List<AdapterClass>, processingEnv: ProcessingEnvironment) {
- infos.forEach({ writeAdapter(it, processingEnv) })
-}
-
-private val GENERATED_PACKAGE = "javax.annotation"
-private val GENERATED_NAME = "Generated"
-private val LIFECYCLE_EVENT = Lifecycle.Event::class.java
-
-private val T = "\$T"
-private val N = "\$N"
-private val L = "\$L"
-private val S = "\$S"
-
-private val OWNER_PARAM: ParameterSpec = ParameterSpec.builder(
- ClassName.get(LifecycleOwner::class.java), "owner").build()
-private val EVENT_PARAM: ParameterSpec = ParameterSpec.builder(
- ClassName.get(LIFECYCLE_EVENT), "event").build()
-private val ON_ANY_PARAM: ParameterSpec = ParameterSpec.builder(TypeName.BOOLEAN, "onAny").build()
-
-private val METHODS_LOGGER: ParameterSpec = ParameterSpec.builder(
- ClassName.get(MethodCallsLogger::class.java), "logger").build()
-
-private const val HAS_LOGGER_VAR = "hasLogger"
-
-private fun writeAdapter(adapter: AdapterClass, processingEnv: ProcessingEnvironment) {
- val receiverField: FieldSpec = FieldSpec.builder(ClassName.get(adapter.type), "mReceiver",
- Modifier.FINAL).build()
- val dispatchMethodBuilder = MethodSpec.methodBuilder("callMethods")
- .returns(TypeName.VOID)
- .addParameter(OWNER_PARAM)
- .addParameter(EVENT_PARAM)
- .addParameter(ON_ANY_PARAM)
- .addParameter(METHODS_LOGGER)
- .addModifiers(Modifier.PUBLIC)
- .addAnnotation(Override::class.java)
- val dispatchMethod = dispatchMethodBuilder.apply {
-
- addStatement("boolean $L = $N != null", HAS_LOGGER_VAR, METHODS_LOGGER)
- val callsByEventType = adapter.calls.groupBy { it.method.onLifecycleEvent.value }
- beginControlFlow("if ($N)", ON_ANY_PARAM).apply {
- writeMethodCalls(callsByEventType[Lifecycle.Event.ON_ANY] ?: emptyList(), receiverField)
- }.endControlFlow()
-
- callsByEventType
- .filterKeys { key -> key != Lifecycle.Event.ON_ANY }
- .forEach { (event, calls) ->
- beginControlFlow("if ($N == $T.$L)", EVENT_PARAM, LIFECYCLE_EVENT, event)
- writeMethodCalls(calls, receiverField)
- endControlFlow()
- }
- }.build()
-
- val receiverParam = ParameterSpec.builder(
- ClassName.get(adapter.type), "receiver").build()
-
- val syntheticMethods = adapter.syntheticMethods.map {
- val method = MethodSpec.methodBuilder(syntheticName(it))
- .returns(TypeName.VOID)
- .addModifiers(Modifier.PUBLIC)
- .addModifiers(Modifier.STATIC)
- .addParameter(receiverParam)
- if (it.parameters.size >= 1) {
- method.addParameter(OWNER_PARAM)
- }
- if (it.parameters.size == 2) {
- method.addParameter(EVENT_PARAM)
- }
-
- val count = it.parameters.size
- val paramString = generateParamString(count)
- method.addStatement("$N.$L($paramString)", receiverParam, it.name(),
- *takeParams(count, OWNER_PARAM, EVENT_PARAM))
- method.build()
- }
-
- val constructor = MethodSpec.constructorBuilder()
- .addParameter(receiverParam)
- .addStatement("this.$N = $N", receiverField, receiverParam)
- .build()
-
- val adapterName = getAdapterName(adapter.type)
- val adapterTypeSpecBuilder = TypeSpec.classBuilder(adapterName)
- .addModifiers(Modifier.PUBLIC)
- .addSuperinterface(ClassName.get(GeneratedAdapter::class.java))
- .addField(receiverField)
- .addMethod(constructor)
- .addMethod(dispatchMethod)
- .addMethods(syntheticMethods)
-
- addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder, processingEnv)
-
- JavaFile.builder(adapter.type.getPackageQName(), adapterTypeSpecBuilder.build())
- .build().writeTo(processingEnv.filer)
-
- generateKeepRule(adapter.type, processingEnv)
-}
-
-private fun addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder: TypeSpec.Builder,
- processingEnv: ProcessingEnvironment) {
- val generatedAnnotationAvailable = processingEnv
- .elementUtils
- .getTypeElement(GENERATED_PACKAGE + "." + GENERATED_NAME) != null
- if (generatedAnnotationAvailable) {
- val generatedAnnotationSpec =
- AnnotationSpec.builder(ClassName.get(GENERATED_PACKAGE, GENERATED_NAME)).addMember(
- "value",
- S,
- LifecycleProcessor::class.java.canonicalName).build()
- adapterTypeSpecBuilder.addAnnotation(generatedAnnotationSpec)
- }
-}
-
-private fun generateKeepRule(type: TypeElement, processingEnv: ProcessingEnvironment) {
- val adapterClass = type.getPackageQName() + "." + getAdapterName(type)
- val observerClass = type.toString()
- val keepRule = """# Generated keep rule for Lifecycle observer adapter.
- |-if class $observerClass {
- | <init>(...);
- |}
- |-keep class $adapterClass {
- | <init>(...);
- |}
- |""".trimMargin()
-
- // Write the keep rule to the META-INF/proguard directory of the Jar file. The file name
- // contains the fully qualified observer name so that file names are unique. This will allow any
- // jar file merging to not overwrite keep rule files.
- val path = "META-INF/proguard/$observerClass.pro"
- val out = processingEnv.filer.createResource(StandardLocation.CLASS_OUTPUT, "", path)
- out.openWriter().use { it.write(keepRule) }
-}
-
-private fun MethodSpec.Builder.writeMethodCalls(calls: List<EventMethodCall>,
- receiverField: FieldSpec) {
- calls.forEach { (method, syntheticAccess) ->
- val count = method.method.parameters.size
- val callType = 1 shl count
- val methodName = method.method.name()
- beginControlFlow("if (!$L || $N.approveCall($S, $callType))",
- HAS_LOGGER_VAR, METHODS_LOGGER, methodName).apply {
-
- if (syntheticAccess == null) {
- val paramString = generateParamString(count)
- addStatement("$N.$L($paramString)", receiverField,
- methodName,
- *takeParams(count, OWNER_PARAM, EVENT_PARAM))
- } else {
- val originalType = syntheticAccess
- val paramString = generateParamString(count + 1)
- val className = ClassName.get(originalType.getPackageQName(),
- getAdapterName(originalType))
- addStatement("$T.$L($paramString)", className,
- syntheticName(method.method),
- *takeParams(count + 1, receiverField, OWNER_PARAM, EVENT_PARAM))
- }
- }.endControlFlow()
- }
- addStatement("return")
-}
-
-private fun takeParams(count: Int, vararg params: Any) = params.take(count).toTypedArray()
-
-private fun generateParamString(count: Int) = (0 until count).joinToString(",") { N }
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/ErrorMessages.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/ErrorMessages.kt
new file mode 100644
index 0000000..5a5df7f
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/ErrorMessages.kt
@@ -0,0 +1,45 @@
+/*
+ * 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 androidx.lifecycle
+
+import androidx.lifecycle.model.EventMethod
+import javax.lang.model.element.TypeElement
+
+object ErrorMessages {
+ const val TOO_MANY_ARGS = "callback method cannot have more than 2 parameters"
+ const val TOO_MANY_ARGS_NOT_ON_ANY = "only callback annotated with ON_ANY " +
+ "can have 2 parameters"
+ const val INVALID_SECOND_ARGUMENT = "2nd argument of a callback method" +
+ " must be Lifecycle.Event and represent the current event"
+ const val INVALID_FIRST_ARGUMENT = "1st argument of a callback method must be " +
+ "a LifecycleOwner which represents the source of the event"
+ const val INVALID_METHOD_MODIFIER = "method marked with OnLifecycleEvent annotation can " +
+ "not be private"
+ const val INVALID_CLASS_MODIFIER = "class containing OnLifecycleEvent methods can not be " +
+ "private"
+ const val INVALID_STATE_OVERRIDE_METHOD = "overridden method must handle the same " +
+ "onState changes as original method"
+ const val INVALID_ENCLOSING_ELEMENT =
+ "Parent of OnLifecycleEvent should be a class or interface"
+ const val INVALID_ANNOTATED_ELEMENT = "OnLifecycleEvent can only be added to methods"
+
+ fun failedToGenerateAdapter(type: TypeElement, failureReason: EventMethod) =
+ """
+ Failed to generate an Adapter for $type, because it needs to be able to access to
+ package private method ${failureReason.method.name()} from ${failureReason.type}
+ """.trim()
+}
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/LifecycleProcessor.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/LifecycleProcessor.kt
new file mode 100644
index 0000000..717afa4
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/LifecycleProcessor.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.lifecycle
+
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.RoundEnvironment
+import javax.annotation.processing.SupportedAnnotationTypes
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.TypeElement
+
+@SupportedAnnotationTypes("androidx.lifecycle.OnLifecycleEvent")
+class LifecycleProcessor : AbstractProcessor() {
+ override fun process(annotations: MutableSet<out TypeElement>,
+ roundEnv: RoundEnvironment): Boolean {
+ val input = collectAndVerifyInput(processingEnv, roundEnv)
+ writeModels(transformToOutput(processingEnv, input), processingEnv)
+ return true
+ }
+
+ override fun getSupportedSourceVersion(): SourceVersion {
+ return SourceVersion.latest()
+ }
+}
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/elements_ext.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/elements_ext.kt
new file mode 100644
index 0000000..9f8adad
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/elements_ext.kt
@@ -0,0 +1,46 @@
+/*
+ * 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 androidx.lifecycle
+
+import com.google.auto.common.MoreElements
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.PackageElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.util.ElementFilter
+
+fun Element.getPackage(): PackageElement = MoreElements.getPackage(this)
+
+fun Element.getPackageQName() = getPackage().qualifiedName.toString()
+
+fun ExecutableElement.name() = simpleName.toString()
+
+fun ExecutableElement.isPackagePrivate() = !modifiers.any {
+ it == Modifier.PUBLIC || it == Modifier.PROTECTED || it == Modifier.PRIVATE
+}
+
+fun ExecutableElement.isProtected() = modifiers.contains(Modifier.PROTECTED)
+
+fun TypeElement.methods(): List<ExecutableElement> = ElementFilter.methodsIn(enclosedElements)
+
+private const val SYNTHETIC = "__synthetic_"
+
+fun syntheticName(method: ExecutableElement) = "$SYNTHETIC${method.simpleName}"
+
+fun isSyntheticMethod(method: ExecutableElement) = method.name().startsWith(SYNTHETIC)
+
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/input_collector.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/input_collector.kt
new file mode 100644
index 0000000..16546f5
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/input_collector.kt
@@ -0,0 +1,168 @@
+/*
+ * 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 androidx.lifecycle
+
+import androidx.lifecycle.model.EventMethod
+import androidx.lifecycle.model.InputModel
+import androidx.lifecycle.model.LifecycleObserverInfo
+import androidx.lifecycle.model.getAdapterName
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.Elements
+import javax.lang.model.util.Types
+import javax.tools.Diagnostic
+
+fun collectAndVerifyInput(processingEnv: ProcessingEnvironment,
+ roundEnv: RoundEnvironment): InputModel {
+ val validator = Validator(processingEnv)
+ val worldCollector = ObserversCollector(processingEnv)
+ val roots = roundEnv.getElementsAnnotatedWith(OnLifecycleEvent::class.java).map { elem ->
+ if (elem.kind != ElementKind.METHOD) {
+ validator.printErrorMessage(ErrorMessages.INVALID_ANNOTATED_ELEMENT, elem)
+ null
+ } else {
+ val enclosingElement = elem.enclosingElement
+ if (validator.validateClass(enclosingElement)) {
+ MoreElements.asType(enclosingElement)
+ } else {
+ null
+ }
+ }
+ }.filterNotNull().toSet()
+ roots.forEach { worldCollector.collect(it) }
+ val observersInfo = worldCollector.observers
+ val generatedAdapters = worldCollector.observers.keys
+ .mapNotNull { type ->
+ worldCollector.generatedAdapterInfoFor(type)?.let { type to it }
+ }.toMap()
+ return InputModel(roots, observersInfo, generatedAdapters)
+}
+
+class ObserversCollector(processingEnv: ProcessingEnvironment) {
+ val typeUtils: Types = processingEnv.typeUtils
+ val elementUtils: Elements = processingEnv.elementUtils
+ val lifecycleObserverTypeMirror: TypeMirror =
+ elementUtils.getTypeElement(LifecycleObserver::class.java.canonicalName).asType()
+ val validator = Validator(processingEnv)
+ val observers: MutableMap<TypeElement, LifecycleObserverInfo> = mutableMapOf()
+
+ fun collect(type: TypeElement): LifecycleObserverInfo? {
+ if (type in observers) {
+ return observers[type]
+ }
+ val parents = (listOf(type.superclass) + type.interfaces)
+ .filter { typeUtils.isAssignable(it, lifecycleObserverTypeMirror) }
+ .filterNot { typeUtils.isSameType(it, lifecycleObserverTypeMirror) }
+ .map { collect(MoreTypes.asTypeElement(it)) }
+ .filterNotNull()
+ val info = createObserverInfo(type, parents)
+ if (info != null) {
+ observers[type] = info
+ }
+ return info
+ }
+
+ fun generatedAdapterInfoFor(type: TypeElement): List<ExecutableElement>? {
+ val packageName = if (type.getPackageQName().isEmpty()) "" else "${type.getPackageQName()}."
+ val adapterType = elementUtils.getTypeElement(packageName + getAdapterName(type))
+ return adapterType?.methods()
+ ?.filter { executable -> isSyntheticMethod(executable) }
+ }
+
+ private fun createObserverInfo(typeElement: TypeElement,
+ parents: List<LifecycleObserverInfo>): LifecycleObserverInfo? {
+ if (!validator.validateClass(typeElement)) {
+ return null
+ }
+ val methods = typeElement.methods().filter { executable ->
+ MoreElements.isAnnotationPresent(executable, OnLifecycleEvent::class.java)
+ }.map { executable ->
+ val onState = executable.getAnnotation(OnLifecycleEvent::class.java)
+ if (validator.validateMethod(executable, onState.value)) {
+ EventMethod(executable, onState, typeElement)
+ } else {
+ null
+ }
+ }.filterNotNull()
+ return LifecycleObserverInfo(typeElement, methods, parents)
+ }
+}
+
+class Validator(val processingEnv: ProcessingEnvironment) {
+
+ fun printErrorMessage(msg: CharSequence, elem: Element) {
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, elem)
+ }
+
+ fun validateParam(param: VariableElement,
+ expectedType: Class<*>, errorMsg: String): Boolean {
+ if (!MoreTypes.isTypeOf(expectedType, param.asType())) {
+ printErrorMessage(errorMsg, param)
+ return false
+ }
+ return true
+ }
+
+ fun validateMethod(method: ExecutableElement, event: Lifecycle.Event): Boolean {
+ if (Modifier.PRIVATE in method.modifiers) {
+ printErrorMessage(ErrorMessages.INVALID_METHOD_MODIFIER, method)
+ return false
+ }
+ val params = method.parameters
+ if ((params.size > 2)) {
+ printErrorMessage(ErrorMessages.TOO_MANY_ARGS, method)
+ return false
+ }
+
+ if (params.size == 2 && event != Lifecycle.Event.ON_ANY) {
+ printErrorMessage(ErrorMessages.TOO_MANY_ARGS_NOT_ON_ANY, method)
+ return false
+ }
+
+ if (params.size == 2 && !validateParam(params[1], Lifecycle.Event::class.java,
+ ErrorMessages.INVALID_SECOND_ARGUMENT)) {
+ return false
+ }
+
+ if (params.size > 0) {
+ return validateParam(params[0], LifecycleOwner::class.java,
+ ErrorMessages.INVALID_FIRST_ARGUMENT)
+ }
+ return true
+ }
+
+ fun validateClass(classElement: Element): Boolean {
+ if (!MoreElements.isType(classElement)) {
+ printErrorMessage(ErrorMessages.INVALID_ENCLOSING_ELEMENT, classElement)
+ return false
+ }
+ if (Modifier.PRIVATE in classElement.modifiers) {
+ printErrorMessage(ErrorMessages.INVALID_CLASS_MODIFIER, classElement)
+ return false
+ }
+ return true
+ }
+}
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/AdapterClass.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/AdapterClass.kt
new file mode 100644
index 0000000..0bd1c5e
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/AdapterClass.kt
@@ -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 androidx.lifecycle.model
+
+import androidx.lifecycle.Lifecycling
+import androidx.lifecycle.getPackage
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.TypeElement
+
+data class AdapterClass(val type: TypeElement,
+ val calls: List<EventMethodCall>,
+ val syntheticMethods: Set<ExecutableElement>)
+
+fun getAdapterName(type: TypeElement): String {
+ val packageElement = type.getPackage()
+ val qName = type.qualifiedName.toString()
+ val partialName = if (packageElement.isUnnamed) qName else qName.substring(
+ packageElement.qualifiedName.toString().length + 1)
+ return Lifecycling.getAdapterName(partialName)
+}
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/EventMethod.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/EventMethod.kt
new file mode 100644
index 0000000..1056e71
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/EventMethod.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.lifecycle.model
+
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.getPackageQName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.TypeElement
+
+data class EventMethod(val method: ExecutableElement,
+ val onLifecycleEvent: OnLifecycleEvent,
+ val type: TypeElement) {
+
+ fun packageName() = type.getPackageQName()
+}
+
+data class EventMethodCall(val method: EventMethod, val syntheticAccess: TypeElement? = null)
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/InputModel.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/InputModel.kt
new file mode 100644
index 0000000..4b3aae9
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/InputModel.kt
@@ -0,0 +1,45 @@
+/*
+ * 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 androidx.lifecycle.model
+
+import androidx.lifecycle.name
+import androidx.lifecycle.syntheticName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.TypeElement
+
+data class InputModel(
+ // all java files with lifecycle annotations excluding classes from classpath
+ private val rootTypes: Set<TypeElement>,
+ // info about all lifecycle observers including classes from classpath
+ val observersInfo: Map<TypeElement, LifecycleObserverInfo>,
+ // info about generated adapters from class path
+ val generatedAdapters: Map<TypeElement, List<ExecutableElement>>) {
+
+ /**
+ * Root class is class defined in currently processed module, not in classpath
+ */
+ fun isRootType(type: TypeElement) = type in rootTypes
+
+ fun hasSyntheticAccessorFor(eventMethod: EventMethod): Boolean {
+ val syntheticMethods = generatedAdapters[eventMethod.type] ?: return false
+ return syntheticMethods.any { executable ->
+ executable.name() == syntheticName(eventMethod.method)
+ // same number + receiver object
+ && (eventMethod.method.parameters.size + 1) == executable.parameters.size
+ }
+ }
+}
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/LifecycleObserverInfo.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/LifecycleObserverInfo.kt
new file mode 100644
index 0000000..59b3c66
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/model/LifecycleObserverInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 androidx.lifecycle.model
+
+import javax.lang.model.element.TypeElement
+
+data class LifecycleObserverInfo(
+ val type: TypeElement,
+ val methods: List<EventMethod>,
+ val parents: List<LifecycleObserverInfo> = listOf())
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/transformation.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/transformation.kt
new file mode 100644
index 0000000..afb1e29
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/transformation.kt
@@ -0,0 +1,143 @@
+/*
+ * 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 androidx.lifecycle
+
+import androidx.lifecycle.model.AdapterClass
+import androidx.lifecycle.model.EventMethod
+import androidx.lifecycle.model.EventMethodCall
+import androidx.lifecycle.model.InputModel
+import androidx.lifecycle.model.LifecycleObserverInfo
+import com.google.common.collect.HashMultimap
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.TypeElement
+import javax.tools.Diagnostic
+
+private fun mergeAndVerifyMethods(processingEnv: ProcessingEnvironment,
+ type: TypeElement,
+ classMethods: List<EventMethod>,
+ parentMethods: List<EventMethod>): List<EventMethod> {
+ // need to update parent methods like that because:
+ // 1. visibility can be expanded
+ // 2. we want to preserve order
+ val updatedParentMethods = parentMethods.map { parentMethod ->
+ val overrideMethod = classMethods.find { (method) ->
+ processingEnv.elementUtils.overrides(method, parentMethod.method, type)
+ }
+ if (overrideMethod != null) {
+ if (overrideMethod.onLifecycleEvent != parentMethod.onLifecycleEvent) {
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR,
+ ErrorMessages.INVALID_STATE_OVERRIDE_METHOD, overrideMethod.method)
+ }
+ overrideMethod
+ } else {
+ parentMethod
+ }
+ }
+ return updatedParentMethods + classMethods.filterNot { updatedParentMethods.contains(it) }
+}
+
+fun flattenObservers(processingEnv: ProcessingEnvironment,
+ world: Map<TypeElement, LifecycleObserverInfo>): List<LifecycleObserverInfo> {
+ val flattened: MutableMap<LifecycleObserverInfo, LifecycleObserverInfo> = mutableMapOf()
+
+ fun traverse(observer: LifecycleObserverInfo) {
+ if (observer in flattened) {
+ return
+ }
+ if (observer.parents.isEmpty()) {
+ flattened[observer] = observer
+ return
+ }
+ observer.parents.forEach(::traverse)
+ val methods = observer.parents
+ .map(flattened::get)
+ .fold(emptyList<EventMethod>()) { list, parentObserver ->
+ mergeAndVerifyMethods(processingEnv, observer.type,
+ parentObserver!!.methods, list)
+ }
+
+ flattened[observer] = LifecycleObserverInfo(observer.type,
+ mergeAndVerifyMethods(processingEnv, observer.type, observer.methods, methods))
+ }
+
+ world.values.forEach(::traverse)
+ return flattened.values.toList()
+}
+
+private fun needsSyntheticAccess(type: TypeElement, eventMethod: EventMethod): Boolean {
+ val executable = eventMethod.method
+ return type.getPackageQName() != eventMethod.packageName()
+ && (executable.isPackagePrivate() || executable.isProtected())
+}
+
+private fun validateMethod(processingEnv: ProcessingEnvironment,
+ world: InputModel, type: TypeElement,
+ eventMethod: EventMethod): Boolean {
+ if (!needsSyntheticAccess(type, eventMethod)) {
+ // no synthetic calls - no problems
+ return true
+ }
+
+ if (world.isRootType(eventMethod.type)) {
+ // we will generate adapters for them, so we can generate all accessors
+ return true
+ }
+
+ if (world.hasSyntheticAccessorFor(eventMethod)) {
+ // previously generated adapter already has synthetic
+ return true
+ }
+
+ processingEnv.messager.printMessage(Diagnostic.Kind.WARNING,
+ ErrorMessages.failedToGenerateAdapter(type, eventMethod), type)
+ return false
+}
+
+fun transformToOutput(processingEnv: ProcessingEnvironment,
+ world: InputModel): List<AdapterClass> {
+ val flatObservers = flattenObservers(processingEnv, world.observersInfo)
+ val syntheticMethods = HashMultimap.create<TypeElement, EventMethodCall>()
+ val adapterCalls = flatObservers
+ // filter out everything that arrived from jars
+ .filter { (type) -> world.isRootType(type) }
+ // filter out if it needs SYNTHETIC access and we can't generate adapter for it
+ .filter { (type, methods) ->
+ methods.all { eventMethod ->
+ validateMethod(processingEnv, world, type, eventMethod)
+ }
+ }
+ .map { (type, methods) ->
+ val calls = methods.map { eventMethod ->
+ if (needsSyntheticAccess(type, eventMethod)) {
+ EventMethodCall(eventMethod, eventMethod.type)
+ } else {
+ EventMethodCall(eventMethod)
+ }
+ }
+ calls.filter { it.syntheticAccess != null }.forEach { eventMethod ->
+ syntheticMethods.put(eventMethod.method.type, eventMethod)
+ }
+ type to calls
+ }.toMap()
+
+ return adapterCalls
+ .map { (type, calls) ->
+ val methods = syntheticMethods.get(type) ?: emptySet()
+ val synthetic = methods.map { eventMethod -> eventMethod!!.method.method }.toSet()
+ AdapterClass(type, calls, synthetic)
+ }
+}
diff --git a/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/writer.kt b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/writer.kt
new file mode 100644
index 0000000..2d2d45d
--- /dev/null
+++ b/lifecycle/compiler/src/main/kotlin/androidx/lifecycle/writer.kt
@@ -0,0 +1,197 @@
+/*
+ * 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 androidx.lifecycle
+
+import androidx.lifecycle.model.AdapterClass
+import androidx.lifecycle.model.EventMethodCall
+import androidx.lifecycle.model.getAdapterName
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.tools.StandardLocation
+
+fun writeModels(infos: List<AdapterClass>, processingEnv: ProcessingEnvironment) {
+ infos.forEach({ writeAdapter(it, processingEnv) })
+}
+
+private val GENERATED_PACKAGE = "javax.annotation"
+private val GENERATED_NAME = "Generated"
+private val LIFECYCLE_EVENT = Lifecycle.Event::class.java
+
+private val T = "\$T"
+private val N = "\$N"
+private val L = "\$L"
+private val S = "\$S"
+
+private val OWNER_PARAM: ParameterSpec = ParameterSpec.builder(
+ ClassName.get(LifecycleOwner::class.java), "owner").build()
+private val EVENT_PARAM: ParameterSpec = ParameterSpec.builder(
+ ClassName.get(LIFECYCLE_EVENT), "event").build()
+private val ON_ANY_PARAM: ParameterSpec = ParameterSpec.builder(TypeName.BOOLEAN, "onAny").build()
+
+private val METHODS_LOGGER: ParameterSpec = ParameterSpec.builder(
+ ClassName.get(MethodCallsLogger::class.java), "logger").build()
+
+private const val HAS_LOGGER_VAR = "hasLogger"
+
+private fun writeAdapter(adapter: AdapterClass, processingEnv: ProcessingEnvironment) {
+ val receiverField: FieldSpec = FieldSpec.builder(ClassName.get(adapter.type), "mReceiver",
+ Modifier.FINAL).build()
+ val dispatchMethodBuilder = MethodSpec.methodBuilder("callMethods")
+ .returns(TypeName.VOID)
+ .addParameter(OWNER_PARAM)
+ .addParameter(EVENT_PARAM)
+ .addParameter(ON_ANY_PARAM)
+ .addParameter(METHODS_LOGGER)
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override::class.java)
+ val dispatchMethod = dispatchMethodBuilder.apply {
+
+ addStatement("boolean $L = $N != null", HAS_LOGGER_VAR, METHODS_LOGGER)
+ val callsByEventType = adapter.calls.groupBy { it.method.onLifecycleEvent.value }
+ beginControlFlow("if ($N)", ON_ANY_PARAM).apply {
+ writeMethodCalls(callsByEventType[Lifecycle.Event.ON_ANY] ?: emptyList(), receiverField)
+ }.endControlFlow()
+
+ callsByEventType
+ .filterKeys { key -> key != Lifecycle.Event.ON_ANY }
+ .forEach { (event, calls) ->
+ beginControlFlow("if ($N == $T.$L)", EVENT_PARAM, LIFECYCLE_EVENT, event)
+ writeMethodCalls(calls, receiverField)
+ endControlFlow()
+ }
+ }.build()
+
+ val receiverParam = ParameterSpec.builder(
+ ClassName.get(adapter.type), "receiver").build()
+
+ val syntheticMethods = adapter.syntheticMethods.map {
+ val method = MethodSpec.methodBuilder(syntheticName(it))
+ .returns(TypeName.VOID)
+ .addModifiers(Modifier.PUBLIC)
+ .addModifiers(Modifier.STATIC)
+ .addParameter(receiverParam)
+ if (it.parameters.size >= 1) {
+ method.addParameter(OWNER_PARAM)
+ }
+ if (it.parameters.size == 2) {
+ method.addParameter(EVENT_PARAM)
+ }
+
+ val count = it.parameters.size
+ val paramString = generateParamString(count)
+ method.addStatement("$N.$L($paramString)", receiverParam, it.name(),
+ *takeParams(count, OWNER_PARAM, EVENT_PARAM))
+ method.build()
+ }
+
+ val constructor = MethodSpec.constructorBuilder()
+ .addParameter(receiverParam)
+ .addStatement("this.$N = $N", receiverField, receiverParam)
+ .build()
+
+ val adapterName = getAdapterName(adapter.type)
+ val adapterTypeSpecBuilder = TypeSpec.classBuilder(adapterName)
+ .addModifiers(Modifier.PUBLIC)
+ .addSuperinterface(ClassName.get(GeneratedAdapter::class.java))
+ .addField(receiverField)
+ .addMethod(constructor)
+ .addMethod(dispatchMethod)
+ .addMethods(syntheticMethods)
+
+ addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder, processingEnv)
+
+ JavaFile.builder(adapter.type.getPackageQName(), adapterTypeSpecBuilder.build())
+ .build().writeTo(processingEnv.filer)
+
+ generateKeepRule(adapter.type, processingEnv)
+}
+
+private fun addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder: TypeSpec.Builder,
+ processingEnv: ProcessingEnvironment) {
+ val generatedAnnotationAvailable = processingEnv
+ .elementUtils
+ .getTypeElement(GENERATED_PACKAGE + "." + GENERATED_NAME) != null
+ if (generatedAnnotationAvailable) {
+ val generatedAnnotationSpec =
+ AnnotationSpec.builder(ClassName.get(GENERATED_PACKAGE, GENERATED_NAME)).addMember(
+ "value",
+ S,
+ LifecycleProcessor::class.java.canonicalName).build()
+ adapterTypeSpecBuilder.addAnnotation(generatedAnnotationSpec)
+ }
+}
+
+private fun generateKeepRule(type: TypeElement, processingEnv: ProcessingEnvironment) {
+ val adapterClass = type.getPackageQName() + "." + getAdapterName(type)
+ val observerClass = type.toString()
+ val keepRule = """# Generated keep rule for Lifecycle observer adapter.
+ |-if class $observerClass {
+ | <init>(...);
+ |}
+ |-keep class $adapterClass {
+ | <init>(...);
+ |}
+ |""".trimMargin()
+
+ // Write the keep rule to the META-INF/proguard directory of the Jar file. The file name
+ // contains the fully qualified observer name so that file names are unique. This will allow any
+ // jar file merging to not overwrite keep rule files.
+ val path = "META-INF/proguard/$observerClass.pro"
+ val out = processingEnv.filer.createResource(StandardLocation.CLASS_OUTPUT, "", path)
+ out.openWriter().use { it.write(keepRule) }
+}
+
+private fun MethodSpec.Builder.writeMethodCalls(calls: List<EventMethodCall>,
+ receiverField: FieldSpec) {
+ calls.forEach { (method, syntheticAccess) ->
+ val count = method.method.parameters.size
+ val callType = 1 shl count
+ val methodName = method.method.name()
+ beginControlFlow("if (!$L || $N.approveCall($S, $callType))",
+ HAS_LOGGER_VAR, METHODS_LOGGER, methodName).apply {
+
+ if (syntheticAccess == null) {
+ val paramString = generateParamString(count)
+ addStatement("$N.$L($paramString)", receiverField,
+ methodName,
+ *takeParams(count, OWNER_PARAM, EVENT_PARAM))
+ } else {
+ val originalType = syntheticAccess
+ val paramString = generateParamString(count + 1)
+ val className = ClassName.get(originalType.getPackageQName(),
+ getAdapterName(originalType))
+ addStatement("$T.$L($paramString)", className,
+ syntheticName(method.method),
+ *takeParams(count + 1, receiverField, OWNER_PARAM, EVENT_PARAM))
+ }
+ }.endControlFlow()
+ }
+ addStatement("return")
+}
+
+private fun takeParams(count: Int, vararg params: Any) = params.take(count).toTypedArray()
+
+private fun generateParamString(count: Int) = (0 until count).joinToString(",") { N }
diff --git a/lifecycle/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/lifecycle/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor
index cdabd71..2acce7d 100644
--- a/lifecycle/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor
+++ b/lifecycle/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -1 +1 @@
-android.arch.lifecycle.LifecycleProcessor
+androidx.lifecycle.LifecycleProcessor
diff --git a/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/InvalidCasesTest.kt b/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/InvalidCasesTest.kt
deleted file mode 100644
index 3e502d3..0000000
--- a/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/InvalidCasesTest.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.arch.lifecycle.utils.processClass
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-class InvalidCasesTest(val name: String, val errorMsg: String) {
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "failingCase({0})")
- fun data() : Collection<Array<Any>> = listOf(
- arrayOf<Any>("foo.InvalidFirstArg1", ErrorMessages.INVALID_FIRST_ARGUMENT),
- arrayOf<Any>("foo.InvalidFirstArg2", ErrorMessages.INVALID_FIRST_ARGUMENT),
- arrayOf<Any>("foo.InvalidSecondArg", ErrorMessages.INVALID_SECOND_ARGUMENT),
- arrayOf<Any>("foo.TooManyArgs1", ErrorMessages.TOO_MANY_ARGS),
- arrayOf<Any>("foo.TooManyArgs2", ErrorMessages.TOO_MANY_ARGS_NOT_ON_ANY),
- arrayOf<Any>("foo.InvalidMethodModifier",
- ErrorMessages.INVALID_METHOD_MODIFIER),
- arrayOf<Any>("foo.InvalidClassModifier", ErrorMessages.INVALID_CLASS_MODIFIER),
- arrayOf<Any>("foo.InvalidInheritance1",
- ErrorMessages.INVALID_STATE_OVERRIDE_METHOD),
- arrayOf<Any>("foo.InvalidInheritance2",
- ErrorMessages.INVALID_STATE_OVERRIDE_METHOD)
- )
- }
-
- @Test
- fun shouldFailWithError() {
- processClass(name).failsToCompile().withErrorContaining(errorMsg)
- }
-}
diff --git a/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/ValidCasesTest.kt b/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/ValidCasesTest.kt
deleted file mode 100644
index 83d69e0..0000000
--- a/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/ValidCasesTest.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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.arch.lifecycle.utils.load
-import android.arch.lifecycle.utils.processClass
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaSourcesSubject
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import java.io.File
-import java.lang.Exception
-import java.net.URLClassLoader
-import javax.tools.StandardLocation
-
-@RunWith(JUnit4::class)
-class ValidCasesTest {
- @Test
- fun testTest() {
- processClass("foo.Bar").compilesWithoutError()
- }
-
- @Test
- fun testOnAny() {
- processClass("foo.OnAnyMethod").compilesWithoutError().and().generatesSources(
- load("foo.OnAnyMethod_LifecycleAdapter", "expected")
- ).and().generatesProGuardRule("foo.OnAnyMethod.pro")
- }
-
- @Test
- fun testInheritance() {
- processClass("foo.InheritanceOk1").compilesWithoutError()
- }
-
- @Test
- fun testInheritance2() {
- processClass("foo.InheritanceOk2").compilesWithoutError().and().generatesSources(
- load("foo.InheritanceOk2Base_LifecycleAdapter", "expected"),
- load("foo.InheritanceOk2Derived_LifecycleAdapter", "expected")
- )
- .and().generatesProGuardRule("foo.InheritanceOk2Base.pro")
- .and().generatesProGuardRule("foo.InheritanceOk2Derived.pro")
- }
-
- @Test
- fun testInheritance3() {
- processClass("foo.InheritanceOk3").compilesWithoutError().and().generatesSources(
- load("foo.InheritanceOk3Base_LifecycleAdapter", "expected"),
- load("foo.InheritanceOk3Derived_LifecycleAdapter", "expected")
- )
- .and().generatesProGuardRule("foo.InheritanceOk3Base.pro")
- .and().generatesProGuardRule("foo.InheritanceOk3Derived.pro")
- }
-
- @Test
- fun testNoPackageClass() {
- processClass("NoPackageOk").compilesWithoutError()
- }
-
- @Test
- fun testInterface1() {
- processClass("foo.InterfaceOk1").compilesWithoutError()
- }
-
- @Test
- fun testInterface2() {
- processClass("foo.InterfaceOk2").compilesWithoutError().and().generatesSources(
- load("foo.InterfaceOk2Base_LifecycleAdapter", "expected"),
- load("foo.InterfaceOk2Derived_LifecycleAdapter", "expected"),
- load("foo.InterfaceOk2Interface_LifecycleAdapter", "expected")
- )
- .and().generatesProGuardRule("foo.InterfaceOk2Base.pro")
- .and().generatesProGuardRule("foo.InterfaceOk2Derived.pro")
- .and().generatesProGuardRule("foo.InterfaceOk2Interface.pro")
- }
-
- @Test
- fun testInheritanceDifferentPackages1() {
- processClass("foo.DifferentPackagesBase1",
- "bar.DifferentPackagesDerived1").compilesWithoutError().and().generatesSources(
- load("foo.DifferentPackagesBase1_LifecycleAdapter", "expected"),
- load("bar.DifferentPackagesDerived1_LifecycleAdapter", "expected")
- )
- .and().generatesProGuardRule("foo.DifferentPackagesBase1.pro")
- .and().generatesProGuardRule("bar.DifferentPackagesDerived1.pro")
- }
-
- @Test
- fun testInheritanceDifferentPackages2() {
- processClass("foo.DifferentPackagesBase2",
- "bar.DifferentPackagesDerived2").compilesWithoutError().and().generatesSources(
- load("foo.DifferentPackagesBase2_LifecycleAdapter", "expected"),
- load("bar.DifferentPackagesDerived2_LifecycleAdapter", "expected")
- )
- .and().generatesProGuardRule("foo.DifferentPackagesPreBase2.pro")
- .and().generatesProGuardRule("foo.DifferentPackagesBase2.pro")
- .and().generatesProGuardRule("bar.DifferentPackagesDerived2.pro")
- }
-
- private fun <T> CompileTester.GeneratedPredicateClause<T>.generatesProGuardRule(name: String):
- CompileTester.SuccessfulFileClause<T> {
- return generatesFileNamed(StandardLocation.CLASS_OUTPUT, "", "META-INF/proguard/$name")
- }
-
- @Test
- fun testJar() {
- JavaSourcesSubject.assertThat(load("foo.DerivedFromJar", ""))
- .withClasspathFrom(libraryClassLoader())
- .processedWith(LifecycleProcessor())
- .compilesWithoutError().and()
- .generatesSources(load("foo.DerivedFromJar_LifecycleAdapter", "expected"))
- }
-
- @Test
- fun testExtendFromJarFailToGenerateAdapter() {
- val compileTester = JavaSourcesSubject.assertThat(load("foo.DerivedFromJar1", ""))
- .withClasspathFrom(libraryClassLoader())
- .processedWith(LifecycleProcessor())
- .compilesWithoutError()
- compileTester.withWarningContaining("Failed to generate an Adapter for")
- doesntGenerateClass(compileTester, "test.library", "ObserverNoAdapter_LifecycleAdapter")
- doesntGenerateClass(compileTester, "foo", "DerivedFromJar1_LifecycleAdapter")
- }
-
- // compile-testing has fancy, but not always convenient API
- private fun doesntGenerateClass(compile: CompileTester.SuccessfulCompilationClause,
- packageName: String, className: String) {
- try {
- compile.and().generatesFileNamed(StandardLocation.CLASS_OUTPUT,
- packageName, "$className.class")
- throw Exception("$packageName.$className shouldn't be generated")
- } catch (e: AssertionError) {
- }
- }
-
- private fun libraryClassLoader(): URLClassLoader {
- val jarUrl = File("src/tests/test-data/lib/test-library.jar").toURI().toURL()
- return URLClassLoader(arrayOf(jarUrl), this.javaClass.classLoader)
- }
-}
diff --git a/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/utils/TestUtils.kt b/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/utils/TestUtils.kt
deleted file mode 100644
index d3b13c2..0000000
--- a/lifecycle/compiler/src/tests/kotlin/android/arch/lifecycle/utils/TestUtils.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2016 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.utils
-
-import android.arch.lifecycle.LifecycleProcessor
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourceSubjectFactory
-import com.google.testing.compile.JavaSourcesSubject
-import java.io.File
-import java.nio.charset.Charset
-import javax.tools.JavaFileObject
-
-fun load(fullClassName: String, folder: String): JavaFileObject {
- val folderPath = "src/tests/test-data/${if (folder.isEmpty()) "" else folder + "/"}"
- val split = fullClassName.split(".")
- val code = File("$folderPath/${split.last()}.java").readText(Charset.defaultCharset())
- return JavaFileObjects.forSourceString(fullClassName, code)
-}
-
-fun processClass(className: String, vararg fullClassNames: String): CompileTester {
- val javaFiles = fullClassNames.map { load(it, "") }.toTypedArray()
- val processedWith = JavaSourcesSubject.assertThat(load(className, ""), *javaFiles)
- .processedWith(LifecycleProcessor())
- return checkNotNull(processedWith)
-}
diff --git a/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/InvalidCasesTest.kt b/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/InvalidCasesTest.kt
new file mode 100644
index 0000000..6a1f114
--- /dev/null
+++ b/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/InvalidCasesTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 androidx.lifecycle
+
+import androidx.lifecycle.utils.processClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class InvalidCasesTest(val name: String, val errorMsg: String) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "failingCase({0})")
+ fun data(): Collection<Array<Any>> = listOf(
+ arrayOf<Any>("foo.InvalidFirstArg1", ErrorMessages.INVALID_FIRST_ARGUMENT),
+ arrayOf<Any>("foo.InvalidFirstArg2", ErrorMessages.INVALID_FIRST_ARGUMENT),
+ arrayOf<Any>("foo.InvalidSecondArg", ErrorMessages.INVALID_SECOND_ARGUMENT),
+ arrayOf<Any>("foo.TooManyArgs1", ErrorMessages.TOO_MANY_ARGS),
+ arrayOf<Any>("foo.TooManyArgs2", ErrorMessages.TOO_MANY_ARGS_NOT_ON_ANY),
+ arrayOf<Any>("foo.InvalidMethodModifier",
+ ErrorMessages.INVALID_METHOD_MODIFIER),
+ arrayOf<Any>("foo.InvalidClassModifier", ErrorMessages.INVALID_CLASS_MODIFIER),
+ arrayOf<Any>("foo.InvalidInheritance1",
+ ErrorMessages.INVALID_STATE_OVERRIDE_METHOD),
+ arrayOf<Any>("foo.InvalidInheritance2",
+ ErrorMessages.INVALID_STATE_OVERRIDE_METHOD)
+ )
+ }
+
+ @Test
+ fun shouldFailWithError() {
+ processClass(name).failsToCompile().withErrorContaining(errorMsg)
+ }
+}
diff --git a/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/ValidCasesTest.kt b/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/ValidCasesTest.kt
new file mode 100644
index 0000000..f6139a0
--- /dev/null
+++ b/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/ValidCasesTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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 androidx.lifecycle
+
+import androidx.lifecycle.utils.load
+import androidx.lifecycle.utils.processClass
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaSourcesSubject
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.lang.Exception
+import java.net.URLClassLoader
+import javax.tools.StandardLocation
+
+@RunWith(JUnit4::class)
+class ValidCasesTest {
+ @Test
+ fun testTest() {
+ processClass("foo.Bar").compilesWithoutError()
+ }
+
+ @Test
+ fun testOnAny() {
+ processClass("foo.OnAnyMethod").compilesWithoutError().and().generatesSources(
+ load("foo.OnAnyMethod_LifecycleAdapter", "expected")
+ ).and().generatesProGuardRule("foo.OnAnyMethod.pro")
+ }
+
+ @Test
+ fun testInheritance() {
+ processClass("foo.InheritanceOk1").compilesWithoutError()
+ }
+
+ @Test
+ fun testInheritance2() {
+ processClass("foo.InheritanceOk2").compilesWithoutError().and().generatesSources(
+ load("foo.InheritanceOk2Base_LifecycleAdapter", "expected"),
+ load("foo.InheritanceOk2Derived_LifecycleAdapter", "expected")
+ )
+ .and().generatesProGuardRule("foo.InheritanceOk2Base.pro")
+ .and().generatesProGuardRule("foo.InheritanceOk2Derived.pro")
+ }
+
+ @Test
+ fun testInheritance3() {
+ processClass("foo.InheritanceOk3").compilesWithoutError().and().generatesSources(
+ load("foo.InheritanceOk3Base_LifecycleAdapter", "expected"),
+ load("foo.InheritanceOk3Derived_LifecycleAdapter", "expected")
+ )
+ .and().generatesProGuardRule("foo.InheritanceOk3Base.pro")
+ .and().generatesProGuardRule("foo.InheritanceOk3Derived.pro")
+ }
+
+ @Test
+ fun testNoPackageClass() {
+ processClass("NoPackageOk").compilesWithoutError()
+ }
+
+ @Test
+ fun testInterface1() {
+ processClass("foo.InterfaceOk1").compilesWithoutError()
+ }
+
+ @Test
+ fun testInterface2() {
+ processClass("foo.InterfaceOk2").compilesWithoutError().and().generatesSources(
+ load("foo.InterfaceOk2Base_LifecycleAdapter", "expected"),
+ load("foo.InterfaceOk2Derived_LifecycleAdapter", "expected"),
+ load("foo.InterfaceOk2Interface_LifecycleAdapter", "expected")
+ )
+ .and().generatesProGuardRule("foo.InterfaceOk2Base.pro")
+ .and().generatesProGuardRule("foo.InterfaceOk2Derived.pro")
+ .and().generatesProGuardRule("foo.InterfaceOk2Interface.pro")
+ }
+
+ @Test
+ fun testInheritanceDifferentPackages1() {
+ processClass("foo.DifferentPackagesBase1",
+ "bar.DifferentPackagesDerived1").compilesWithoutError().and().generatesSources(
+ load("foo.DifferentPackagesBase1_LifecycleAdapter", "expected"),
+ load("bar.DifferentPackagesDerived1_LifecycleAdapter", "expected")
+ )
+ .and().generatesProGuardRule("foo.DifferentPackagesBase1.pro")
+ .and().generatesProGuardRule("bar.DifferentPackagesDerived1.pro")
+ }
+
+ @Test
+ fun testInheritanceDifferentPackages2() {
+ processClass("foo.DifferentPackagesBase2",
+ "bar.DifferentPackagesDerived2").compilesWithoutError().and().generatesSources(
+ load("foo.DifferentPackagesBase2_LifecycleAdapter", "expected"),
+ load("bar.DifferentPackagesDerived2_LifecycleAdapter", "expected")
+ )
+ .and().generatesProGuardRule("foo.DifferentPackagesPreBase2.pro")
+ .and().generatesProGuardRule("foo.DifferentPackagesBase2.pro")
+ .and().generatesProGuardRule("bar.DifferentPackagesDerived2.pro")
+ }
+
+ private fun <T> CompileTester.GeneratedPredicateClause<T>.generatesProGuardRule(name: String):
+ CompileTester.SuccessfulFileClause<T> {
+ return generatesFileNamed(StandardLocation.CLASS_OUTPUT, "", "META-INF/proguard/$name")
+ }
+
+ @Test
+ fun testJar() {
+ JavaSourcesSubject.assertThat(load("foo.DerivedFromJar", ""))
+ .withClasspathFrom(libraryClassLoader())
+ .processedWith(LifecycleProcessor())
+ .compilesWithoutError().and()
+ .generatesSources(load("foo.DerivedFromJar_LifecycleAdapter", "expected"))
+ }
+
+ @Test
+ fun testExtendFromJarFailToGenerateAdapter() {
+ val compileTester = JavaSourcesSubject.assertThat(load("foo.DerivedFromJar1", ""))
+ .withClasspathFrom(libraryClassLoader())
+ .processedWith(LifecycleProcessor())
+ .compilesWithoutError()
+ compileTester.withWarningContaining("Failed to generate an Adapter for")
+ doesntGenerateClass(compileTester, "test.library", "ObserverNoAdapter_LifecycleAdapter")
+ doesntGenerateClass(compileTester, "foo", "DerivedFromJar1_LifecycleAdapter")
+ }
+
+ // compile-testing has fancy, but not always convenient API
+ private fun doesntGenerateClass(compile: CompileTester.SuccessfulCompilationClause,
+ packageName: String, className: String) {
+ try {
+ compile.and().generatesFileNamed(StandardLocation.CLASS_OUTPUT,
+ packageName, "$className.class")
+ throw Exception("$packageName.$className shouldn't be generated")
+ } catch (e: AssertionError) {
+ }
+ }
+
+ private fun libraryClassLoader(): URLClassLoader {
+ val jarUrl = File("src/tests/test-data/lib/test-library.jar").toURI().toURL()
+ return URLClassLoader(arrayOf(jarUrl), this.javaClass.classLoader)
+ }
+}
diff --git a/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/utils/TestUtils.kt b/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/utils/TestUtils.kt
new file mode 100644
index 0000000..b29a2f0
--- /dev/null
+++ b/lifecycle/compiler/src/tests/kotlin/androidx/lifecycle/utils/TestUtils.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.utils
+
+import androidx.lifecycle.LifecycleProcessor
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubject
+import java.io.File
+import java.nio.charset.Charset
+import javax.tools.JavaFileObject
+
+fun load(fullClassName: String, folder: String): JavaFileObject {
+ val folderPath = "src/tests/test-data/${if (folder.isEmpty()) "" else folder + "/"}"
+ val split = fullClassName.split(".")
+ val code = File("$folderPath/${split.last()}.java").readText(Charset.defaultCharset())
+ return JavaFileObjects.forSourceString(fullClassName, code)
+}
+
+fun processClass(className: String, vararg fullClassNames: String): CompileTester {
+ val javaFiles = fullClassNames.map { load(it, "") }.toTypedArray()
+ val processedWith = JavaSourcesSubject.assertThat(load(className, ""), *javaFiles)
+ .processedWith(LifecycleProcessor())
+ return checkNotNull(processedWith)
+}
diff --git a/lifecycle/compiler/src/tests/test-data/Bar.java b/lifecycle/compiler/src/tests/test-data/Bar.java
index cfc41ca..64c7afc 100644
--- a/lifecycle/compiler/src/tests/test-data/Bar.java
+++ b/lifecycle/compiler/src/tests/test-data/Bar.java
@@ -1,12 +1,12 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class Bar implements LifecycleObserver {
@OnLifecycleEvent(ON_START)
diff --git a/lifecycle/compiler/src/tests/test-data/DerivedFromJar.java b/lifecycle/compiler/src/tests/test-data/DerivedFromJar.java
index d503903..834925d 100644
--- a/lifecycle/compiler/src/tests/test-data/DerivedFromJar.java
+++ b/lifecycle/compiler/src/tests/test-data/DerivedFromJar.java
@@ -1,9 +1,9 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.OnLifecycleEvent;
public class DerivedFromJar extends test.library.LibraryBaseObserver {
@OnLifecycleEvent(ON_START)
diff --git a/lifecycle/compiler/src/tests/test-data/DerivedFromJar1.java b/lifecycle/compiler/src/tests/test-data/DerivedFromJar1.java
index efcfaff..f669761 100644
--- a/lifecycle/compiler/src/tests/test-data/DerivedFromJar1.java
+++ b/lifecycle/compiler/src/tests/test-data/DerivedFromJar1.java
@@ -16,10 +16,10 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.OnLifecycleEvent;
public class DerivedFromJar1 extends test.library.PPObserverNoAdapter {
@OnLifecycleEvent(ON_START)
diff --git a/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase1.java b/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase1.java
index fb0966d..82c0699 100644
--- a/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase1.java
+++ b/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase1.java
@@ -16,12 +16,12 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class DifferentPackagesBase1 implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase2.java b/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase2.java
index 1c3ea72..8343a1c 100644
--- a/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase2.java
+++ b/lifecycle/compiler/src/tests/test-data/DifferentPackagesBase2.java
@@ -16,12 +16,12 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
class DifferentPackagesPreBase2 implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived1.java b/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived1.java
index 8ad4c91..5c218b5 100644
--- a/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived1.java
+++ b/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived1.java
@@ -16,11 +16,11 @@
package bar;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
import foo.DifferentPackagesBase1;
public class DifferentPackagesDerived1 extends DifferentPackagesBase1 {
diff --git a/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived2.java b/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived2.java
index 648f628..a75f227 100644
--- a/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived2.java
+++ b/lifecycle/compiler/src/tests/test-data/DifferentPackagesDerived2.java
@@ -16,11 +16,11 @@
package bar;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
import foo.DifferentPackagesBase2;
public class DifferentPackagesDerived2 extends DifferentPackagesBase2 {
diff --git a/lifecycle/compiler/src/tests/test-data/InheritanceOk1.java b/lifecycle/compiler/src/tests/test-data/InheritanceOk1.java
index 889c463..d111439 100644
--- a/lifecycle/compiler/src/tests/test-data/InheritanceOk1.java
+++ b/lifecycle/compiler/src/tests/test-data/InheritanceOk1.java
@@ -16,12 +16,12 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
class Base1 implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InheritanceOk2.java b/lifecycle/compiler/src/tests/test-data/InheritanceOk2.java
index bb00cca..ade9a86 100644
--- a/lifecycle/compiler/src/tests/test-data/InheritanceOk2.java
+++ b/lifecycle/compiler/src/tests/test-data/InheritanceOk2.java
@@ -1,11 +1,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
class InheritanceOk2Base implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InheritanceOk3.java b/lifecycle/compiler/src/tests/test-data/InheritanceOk3.java
index 5a3c91d..b429848 100644
--- a/lifecycle/compiler/src/tests/test-data/InheritanceOk3.java
+++ b/lifecycle/compiler/src/tests/test-data/InheritanceOk3.java
@@ -16,12 +16,12 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
class InheritanceOk3Base implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InterfaceOk1.java b/lifecycle/compiler/src/tests/test-data/InterfaceOk1.java
index 8450302..d80e6d9 100644
--- a/lifecycle/compiler/src/tests/test-data/InterfaceOk1.java
+++ b/lifecycle/compiler/src/tests/test-data/InterfaceOk1.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
interface InterfaceOk1 extends LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InterfaceOk2.java b/lifecycle/compiler/src/tests/test-data/InterfaceOk2.java
index fee5b10..5fc2b92 100644
--- a/lifecycle/compiler/src/tests/test-data/InterfaceOk2.java
+++ b/lifecycle/compiler/src/tests/test-data/InterfaceOk2.java
@@ -16,12 +16,12 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
class InterfaceOk2Base implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidClassModifier.java b/lifecycle/compiler/src/tests/test-data/InvalidClassModifier.java
index 919bb8c..c93fe13 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidClassModifier.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidClassModifier.java
@@ -16,11 +16,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
public class InvalidClassModifier {
private static class Inner implements LifecycleObserver {
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidFirstArg1.java b/lifecycle/compiler/src/tests/test-data/InvalidFirstArg1.java
index b028443..2ecb0ec 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidFirstArg1.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidFirstArg1.java
@@ -1,10 +1,10 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
public class InvalidFirstArg1 implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidFirstArg2.java b/lifecycle/compiler/src/tests/test-data/InvalidFirstArg2.java
index d9937ac..3d7b3bf 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidFirstArg2.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidFirstArg2.java
@@ -1,10 +1,10 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
+import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
public class InvalidFirstArg2 implements LifecycleObserver {
@OnLifecycleEvent(ON_ANY)
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidInheritance1.java b/lifecycle/compiler/src/tests/test-data/InvalidInheritance1.java
index d8f0f62..e0c099a 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidInheritance1.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidInheritance1.java
@@ -16,11 +16,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
class Base implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidInheritance2.java b/lifecycle/compiler/src/tests/test-data/InvalidInheritance2.java
index b63ad44..c3c8cac 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidInheritance2.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidInheritance2.java
@@ -16,11 +16,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
interface Base extends LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidMethodModifier.java b/lifecycle/compiler/src/tests/test-data/InvalidMethodModifier.java
index 5eed7b8..00d817d 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidMethodModifier.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidMethodModifier.java
@@ -16,11 +16,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
public class InvalidMethodModifier implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/InvalidSecondArg.java b/lifecycle/compiler/src/tests/test-data/InvalidSecondArg.java
index 60ad464..07df549 100644
--- a/lifecycle/compiler/src/tests/test-data/InvalidSecondArg.java
+++ b/lifecycle/compiler/src/tests/test-data/InvalidSecondArg.java
@@ -1,10 +1,10 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
+import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class InvalidSecondArg implements LifecycleObserver {
@OnLifecycleEvent(ON_ANY)
diff --git a/lifecycle/compiler/src/tests/test-data/NoPackageOk.java b/lifecycle/compiler/src/tests/test-data/NoPackageOk.java
index e7a84ef..e972151 100644
--- a/lifecycle/compiler/src/tests/test-data/NoPackageOk.java
+++ b/lifecycle/compiler/src/tests/test-data/NoPackageOk.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
class NoPackageOk implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/OnAnyMethod.java b/lifecycle/compiler/src/tests/test-data/OnAnyMethod.java
index 8761774..22b8c07 100644
--- a/lifecycle/compiler/src/tests/test-data/OnAnyMethod.java
+++ b/lifecycle/compiler/src/tests/test-data/OnAnyMethod.java
@@ -16,13 +16,13 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class OnAnyMethod implements LifecycleObserver {
diff --git a/lifecycle/compiler/src/tests/test-data/TooManyArgs1.java b/lifecycle/compiler/src/tests/test-data/TooManyArgs1.java
index cdcff60..4599ecd 100644
--- a/lifecycle/compiler/src/tests/test-data/TooManyArgs1.java
+++ b/lifecycle/compiler/src/tests/test-data/TooManyArgs1.java
@@ -1,11 +1,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
+import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class TooManyArgs1 implements LifecycleObserver {
@OnLifecycleEvent(ON_ANY)
diff --git a/lifecycle/compiler/src/tests/test-data/TooManyArgs2.java b/lifecycle/compiler/src/tests/test-data/TooManyArgs2.java
index b38d906..b4fe75b 100644
--- a/lifecycle/compiler/src/tests/test-data/TooManyArgs2.java
+++ b/lifecycle/compiler/src/tests/test-data/TooManyArgs2.java
@@ -1,11 +1,11 @@
package foo;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class TooManyArgs2 implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar1_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar1_LifecycleAdapter.java
index 2ff2db2..99642cf 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar1_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar1_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class DerivedFromJar1_LifecycleAdapter implements GeneratedAdapter {
final DerivedFromJar1 mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar_LifecycleAdapter.java
index 191499d..d506d6d 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/DerivedFromJar_LifecycleAdapter.java
@@ -16,15 +16,15 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
import test.library.LibraryBaseObserver_LifecycleAdapter;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class DerivedFromJar_LifecycleAdapter implements GeneratedAdapter {
final DerivedFromJar mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase1_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase1_LifecycleAdapter.java
index 9b9c9ea..b29c24e 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase1_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase1_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class DifferentPackagesBase1_LifecycleAdapter implements GeneratedAdapter {
final DifferentPackagesBase1 mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase2_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase2_LifecycleAdapter.java
index d98b5e4..13bb302 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase2_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesBase2_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class DifferentPackagesBase2_LifecycleAdapter implements GeneratedAdapter {
final DifferentPackagesBase2 mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived1_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived1_LifecycleAdapter.java
index 4ff134f..eacb58a 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived1_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived1_LifecycleAdapter.java
@@ -16,15 +16,15 @@
package bar;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import foo.DifferentPackagesBase1_LifecycleAdapter;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class DifferentPackagesDerived1_LifecycleAdapter implements GeneratedAdapter {
final DifferentPackagesDerived1 mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived2_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived2_LifecycleAdapter.java
index 4132f95..5dc1d9b 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived2_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/DifferentPackagesDerived2_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package bar;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class DifferentPackagesDerived2_LifecycleAdapter implements GeneratedAdapter {
final DifferentPackagesDerived2 mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Base_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Base_LifecycleAdapter.java
index 2013e46..25a6353 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Base_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Base_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InheritanceOk2Base_LifecycleAdapter implements GeneratedAdapter {
final InheritanceOk2Base mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Derived_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Derived_LifecycleAdapter.java
index 62a631f..a673cc3 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Derived_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk2Derived_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InheritanceOk2Derived_LifecycleAdapter implements GeneratedAdapter {
final InheritanceOk2Derived mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Base_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Base_LifecycleAdapter.java
index 4b545d9..d7ac84b 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Base_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Base_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InheritanceOk3Base_LifecycleAdapter implements GeneratedAdapter {
final InheritanceOk3Base mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Derived_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Derived_LifecycleAdapter.java
index 643617c..ab5d512 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Derived_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InheritanceOk3Derived_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InheritanceOk3Derived_LifecycleAdapter implements GeneratedAdapter {
final InheritanceOk3Derived mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Base_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Base_LifecycleAdapter.java
index 59aa5a2..bc580ca 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Base_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Base_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InterfaceOk2Base_LifecycleAdapter implements GeneratedAdapter {
final InterfaceOk2Base mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Derived_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Derived_LifecycleAdapter.java
index e04dec6..04d13e3 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Derived_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Derived_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InterfaceOk2Derived_LifecycleAdapter implements GeneratedAdapter {
final InterfaceOk2Derived mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Interface_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Interface_LifecycleAdapter.java
index 268f68b..dab1a1a 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Interface_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/InterfaceOk2Interface_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class InterfaceOk2Interface_LifecycleAdapter implements GeneratedAdapter {
final InterfaceOk2Interface mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/ObserverNoAdapter_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/ObserverNoAdapter_LifecycleAdapter.java
index c2dc1b6..0956a80 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/ObserverNoAdapter_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/ObserverNoAdapter_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package test.library;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class ObserverNoAdapter_LifecycleAdapter implements GeneratedAdapter {
final ObserverNoAdapter mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/expected/OnAnyMethod_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/expected/OnAnyMethod_LifecycleAdapter.java
index ba7ccc1..39a1768 100644
--- a/lifecycle/compiler/src/tests/test-data/expected/OnAnyMethod_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/expected/OnAnyMethod_LifecycleAdapter.java
@@ -16,14 +16,14 @@
package foo;
-import android.arch.lifecycle.GeneratedAdapter;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MethodCallsLogger;
+import androidx.lifecycle.GeneratedAdapter;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MethodCallsLogger;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class OnAnyMethod_LifecycleAdapter implements GeneratedAdapter {
final OnAnyMethod mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver.java b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver.java
index c3e83e7..94d3643 100644
--- a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver.java
+++ b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver.java
@@ -16,13 +16,13 @@
package test.library;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class LibraryBaseObserver implements LifecycleObserver {
@OnLifecycleEvent(ON_START)
diff --git a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver_LifecycleAdapter.java b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver_LifecycleAdapter.java
index ea7e631..5412e29 100644
--- a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver_LifecycleAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/LibraryBaseObserver_LifecycleAdapter.java
@@ -16,13 +16,13 @@
package test.library;
-import android.arch.lifecycle.GenericLifecycleObserver;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
+import androidx.lifecycle.GenericLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
import java.lang.Override;
import javax.annotation.Generated;
-@Generated("android.arch.lifecycle.LifecycleProcessor")
+@Generated("androidx.lifecycle.LifecycleProcessor")
public class LibraryBaseObserver_LifecycleAdapter implements GenericLifecycleObserver {
final LibraryBaseObserver mReceiver;
diff --git a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/ObserverNoAdapter.java b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/ObserverNoAdapter.java
index ea000f7..75004ac 100644
--- a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/ObserverNoAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/ObserverNoAdapter.java
@@ -16,12 +16,12 @@
package test.library;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class ObserverNoAdapter implements LifecycleObserver {
@OnLifecycleEvent(ON_STOP)
diff --git a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverNoAdapter.java b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverNoAdapter.java
index 06e10b5..382e3d8 100644
--- a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverNoAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverNoAdapter.java
@@ -16,13 +16,13 @@
package test.library;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class PPObserverNoAdapter implements LifecycleObserver {
@OnLifecycleEvent(ON_START)
diff --git a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverWithAdapter.java b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverWithAdapter.java
index 25a9b5d..828e9c0 100644
--- a/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverWithAdapter.java
+++ b/lifecycle/compiler/src/tests/test-data/lib/src/test/library/PPObserverWithAdapter.java
@@ -16,13 +16,13 @@
package test.library;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
public class PPObserverWithAdapter implements LifecycleObserver {
@OnLifecycleEvent(ON_START)
diff --git a/lifecycle/compiler/src/tests/test-data/lib/test-library.jar b/lifecycle/compiler/src/tests/test-data/lib/test-library.jar
index cd9ac8b..587dbd5 100644
--- a/lifecycle/compiler/src/tests/test-data/lib/test-library.jar
+++ b/lifecycle/compiler/src/tests/test-data/lib/test-library.jar
Binary files differ
diff --git a/lifecycle/extensions/api/current.txt b/lifecycle/extensions/api/current.txt
index 8bd7d59..dad4ab8 100644
--- a/lifecycle/extensions/api/current.txt
+++ b/lifecycle/extensions/api/current.txt
@@ -1,20 +1,20 @@
-package android.arch.lifecycle {
+package androidx.lifecycle {
- public class LifecycleService extends android.app.Service implements android.arch.lifecycle.LifecycleOwner {
+ public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
ctor public LifecycleService();
- method public android.arch.lifecycle.Lifecycle getLifecycle();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
method public android.os.IBinder onBind(android.content.Intent);
method public void onStart(android.content.Intent, int);
}
- public class ProcessLifecycleOwner implements android.arch.lifecycle.LifecycleOwner {
- method public static android.arch.lifecycle.LifecycleOwner get();
- method public android.arch.lifecycle.Lifecycle getLifecycle();
+ public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+ method public static androidx.lifecycle.LifecycleOwner get();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
}
public class ServiceLifecycleDispatcher {
- ctor public ServiceLifecycleDispatcher(android.arch.lifecycle.LifecycleOwner);
- method public android.arch.lifecycle.Lifecycle getLifecycle();
+ ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner);
+ method public androidx.lifecycle.Lifecycle getLifecycle();
method public void onServicePreSuperOnBind();
method public void onServicePreSuperOnCreate();
method public void onServicePreSuperOnDestroy();
@@ -23,19 +23,19 @@
public class ViewModelProviders {
ctor public deprecated ViewModelProviders();
- method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.Fragment);
- method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.FragmentActivity);
- method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.Fragment, android.arch.lifecycle.ViewModelProvider.Factory);
- method public static android.arch.lifecycle.ViewModelProvider of(android.support.v4.app.FragmentActivity, android.arch.lifecycle.ViewModelProvider.Factory);
+ method public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment);
+ method public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity);
+ method public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.Fragment, androidx.lifecycle.ViewModelProvider.Factory);
+ method public static androidx.lifecycle.ViewModelProvider of(androidx.fragment.app.FragmentActivity, androidx.lifecycle.ViewModelProvider.Factory);
}
- public static deprecated class ViewModelProviders.DefaultFactory extends android.arch.lifecycle.ViewModelProvider.AndroidViewModelFactory {
+ public static deprecated class ViewModelProviders.DefaultFactory extends androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory {
ctor public deprecated ViewModelProviders.DefaultFactory(android.app.Application);
}
public class ViewModelStores {
- method public static android.arch.lifecycle.ViewModelStore of(android.support.v4.app.FragmentActivity);
- method public static android.arch.lifecycle.ViewModelStore of(android.support.v4.app.Fragment);
+ method public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.FragmentActivity);
+ method public static androidx.lifecycle.ViewModelStore of(androidx.fragment.app.Fragment);
}
}
diff --git a/lifecycle/extensions/api/1.0.0.ignore b/lifecycle/extensions/api_legacy/1.0.0.ignore
similarity index 100%
rename from lifecycle/extensions/api/1.0.0.ignore
rename to lifecycle/extensions/api_legacy/1.0.0.ignore
diff --git a/lifecycle/extensions/api/1.0.0.txt b/lifecycle/extensions/api_legacy/1.0.0.txt
similarity index 100%
rename from lifecycle/extensions/api/1.0.0.txt
rename to lifecycle/extensions/api_legacy/1.0.0.txt
diff --git a/lifecycle/extensions/api/1.1.0.txt b/lifecycle/extensions/api_legacy/1.1.0.txt
similarity index 100%
rename from lifecycle/extensions/api/1.1.0.txt
rename to lifecycle/extensions/api_legacy/1.1.0.txt
diff --git a/lifecycle/extensions/api/1.1.0.txt b/lifecycle/extensions/api_legacy/current.txt
similarity index 100%
copy from lifecycle/extensions/api/1.1.0.txt
copy to lifecycle/extensions/api_legacy/current.txt
diff --git a/lifecycle/extensions/build.gradle b/lifecycle/extensions/build.gradle
index 80b7fc8..a2be25d 100644
--- a/lifecycle/extensions/build.gradle
+++ b/lifecycle/extensions/build.gradle
@@ -30,13 +30,13 @@
}
dependencies {
- api(project(":lifecycle:runtime"))
- api(project(":arch:common"))
- api(project(":arch:runtime"))
+ api(project(":lifecycle:lifecycle-runtime"))
+ api(project(":arch:core-common"))
+ api(project(":arch:core-runtime"))
api(SUPPORT_FRAGMENTS, libs.support_exclude_config)
- api(project(":lifecycle:common"))
- api(project(":lifecycle:livedata"))
- api(project(":lifecycle:viewmodel"))
+ api(project(":lifecycle:lifecycle-common"))
+ api(project(":lifecycle:lifecycle-livedata"))
+ api(project(":lifecycle:lifecycle-viewmodel"))
testImplementation(project(":arch:core-testing"))
testImplementation(JUNIT)
diff --git a/lifecycle/extensions/proguard-rules.pro b/lifecycle/extensions/proguard-rules.pro
index 68db05a..c2d2146 100644
--- a/lifecycle/extensions/proguard-rules.pro
+++ b/lifecycle/extensions/proguard-rules.pro
@@ -1,3 +1,3 @@
--keep class * extends android.arch.lifecycle.AndroidViewModel {
+-keep class * extends androidx.lifecycle.AndroidViewModel {
<init>(android.app.Application);
}
\ No newline at end of file
diff --git a/lifecycle/extensions/src/androidTest/AndroidManifest.xml b/lifecycle/extensions/src/androidTest/AndroidManifest.xml
index db921fd..d0f5483 100644
--- a/lifecycle/extensions/src/androidTest/AndroidManifest.xml
+++ b/lifecycle/extensions/src/androidTest/AndroidManifest.xml
@@ -16,18 +16,18 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.extensions.test">
+ package="androidx.lifecycle.extensions.test">
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<application>
- <activity android:name="android.arch.lifecycle.viewmodeltest.ViewModelActivity"
+ <activity android:name="androidx.lifecycle.viewmodeltest.ViewModelActivity"
android:theme="@style/Base.Theme.AppCompat">
</activity>
- <activity android:name="android.arch.lifecycle.EmptyActivity"/>
- <activity android:name="android.arch.lifecycle.activity.FragmentLifecycleActivity"
+ <activity android:name="androidx.lifecycle.EmptyActivity"/>
+ <activity android:name="androidx.lifecycle.activity.FragmentLifecycleActivity"
android:theme="@style/Base.Theme.AppCompat"/>
- <activity android:name="android.arch.lifecycle.activity.EmptyActivity"/>
- <service android:name="android.arch.lifecycle.service.TestService"/>
+ <activity android:name="androidx.lifecycle.activity.EmptyActivity"/>
+ <service android:name="androidx.lifecycle.service.TestService"/>
</application>
</manifest>
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/EmptyActivity.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/EmptyActivity.java
deleted file mode 100644
index 738bd66..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/EmptyActivity.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.v4.app.FragmentActivity;
-
-public class EmptyActivity extends FragmentActivity {
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/FragmentLifecycleInActivityTest.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/FragmentLifecycleInActivityTest.java
deleted file mode 100644
index 70a4465..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/FragmentLifecycleInActivityTest.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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 org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.app.Instrumentation;
-import android.arch.lifecycle.activity.FragmentLifecycleActivity;
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v4.app.Fragment;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-
-@MediumTest
-@RunWith(Parameterized.class)
-public class FragmentLifecycleInActivityTest {
-
- private static final long TIMEOUT = 2; //sec
-
- @Rule
- public ActivityTestRule<FragmentLifecycleActivity> mActivityRule =
- new ActivityTestRule<>(FragmentLifecycleActivity.class, false, false);
-
- private Instrumentation mInstrumentation;
-
- @SuppressWarnings("WeakerAccess")
- @Parameterized.Parameter
- public boolean mNested;
-
- @Parameterized.Parameters(name = "nested_{0}")
- public static Object[][] params() {
- return new Object[][]{new Object[]{false}, new Object[]{true}};
- }
-
- @Before
- public void getInstrumentation() {
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
- }
-
- private void reset() {
- mActivityRule.getActivity().resetEvents();
- }
-
- @Test
- public void testFullEvents() throws Throwable {
- final FragmentLifecycleActivity activity = launchActivity();
- waitForIdle();
- assertEvents(ON_CREATE, ON_START, ON_RESUME);
- reset();
- finishActivity(activity);
- assertEvents(ON_PAUSE, ON_STOP, ON_DESTROY);
- }
-
- @Test
- public void testStopStart() throws Throwable {
- final FragmentLifecycleActivity activity = launchActivity();
- waitForIdle();
- assertEvents(ON_CREATE, ON_START, ON_RESUME);
- reset();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mInstrumentation.callActivityOnPause(activity);
- mInstrumentation.callActivityOnStop(activity);
- }
- });
- waitForIdle();
- assertEvents(ON_PAUSE, ON_STOP);
- reset();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mInstrumentation.callActivityOnStart(activity);
- mInstrumentation.callActivityOnResume(activity);
- }
- });
- waitForIdle();
- assertEvents(ON_START, ON_RESUME);
- }
-
- private FragmentLifecycleActivity launchActivity() throws Throwable {
- Intent intent = FragmentLifecycleActivity.intentFor(mInstrumentation.getTargetContext(),
- mNested);
- final FragmentLifecycleActivity activity = mActivityRule.launchActivity(intent);
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Fragment main = activity.getSupportFragmentManager()
- .findFragmentByTag(FragmentLifecycleActivity.MAIN_TAG);
- assertThat("test sanity", main, notNullValue());
- Fragment nestedFragment = main.getChildFragmentManager()
- .findFragmentByTag(FragmentLifecycleActivity.NESTED_TAG);
- assertThat("test sanity", nestedFragment != null, is(mNested));
- }
- });
- assertThat(activity.getObservedOwner(), instanceOf(
- mNested ? FragmentLifecycleActivity.NestedFragment.class
- : FragmentLifecycleActivity.MainFragment.class
- ));
- return activity;
- }
-
- private void waitForIdle() {
- mInstrumentation.waitForIdleSync();
- }
-
- private void finishActivity(final FragmentLifecycleActivity activity)
- throws InterruptedException {
- mInstrumentation.runOnMainSync(new Runnable() {
- @Override
- public void run() {
- activity.finish();
- }
- });
- assertThat(activity.awaitForDestruction(TIMEOUT, TimeUnit.SECONDS), is(true));
- }
-
- private void assertEvents(Lifecycle.Event... events) {
- assertThat(mActivityRule.getActivity().getLoggedEvents(), is(Arrays.asList(events)));
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/FragmentOperationsLifecycleTest.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/FragmentOperationsLifecycleTest.java
deleted file mode 100644
index 3e61277..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/FragmentOperationsLifecycleTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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 org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import static java.util.Arrays.asList;
-
-import android.arch.lifecycle.activity.EmptyActivity;
-import android.arch.lifecycle.extensions.test.R;
-import android.support.test.annotation.UiThreadTest;
-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;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class FragmentOperationsLifecycleTest {
-
- @Rule
- public ActivityTestRule<EmptyActivity> mActivityTestRule = new ActivityTestRule<>(
- EmptyActivity.class);
-
- @Test
- @UiThreadTest
- public void addRemoveFragment() {
- EmptyActivity activity = mActivityTestRule.getActivity();
- Fragment fragment = new Fragment();
- FragmentManager fm = activity.getSupportFragmentManager();
- fm.beginTransaction().add(fragment, "tag").commitNow();
- CollectingObserver observer = observeAndCollectIn(fragment);
- assertThat(observer.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
- fm.beginTransaction().remove(fragment).commitNow();
- assertThat(observer.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
- fm.beginTransaction().add(fragment, "tag").commitNow();
- assertThat(observer.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
- }
-
- @Test
- @UiThreadTest
- public void fragmentInBackstack() {
- EmptyActivity activity = mActivityTestRule.getActivity();
- Fragment fragment1 = new Fragment();
- FragmentManager fm = activity.getSupportFragmentManager();
- fm.beginTransaction().add(R.id.fragment_container, fragment1, "tag").addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- CollectingObserver observer1 = observeAndCollectIn(fragment1);
- assertThat(observer1.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
-
- Fragment fragment2 = new Fragment();
- fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- CollectingObserver observer2 = observeAndCollectIn(fragment2);
- assertThat(observer1.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP)));
- assertThat(observer2.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
-
- assertThat(fm.popBackStackImmediate(), is(true));
- assertThat(observer1.getEventsAndReset(), is(asList(ON_START, ON_RESUME)));
- assertThat(observer2.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
-
- assertThat(fm.popBackStackImmediate(), is(true));
- assertThat(observer1.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
- }
-
- private static CollectingObserver observeAndCollectIn(Fragment fragment) {
- CollectingObserver observer = new CollectingObserver();
- fragment.getLifecycle().addObserver(observer);
- return observer;
- }
-
- private static class CollectingObserver implements LifecycleObserver {
- final List<Lifecycle.Event> mCollectedEvents = new ArrayList<>();
-
- @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- public void anyEvent(LifecycleOwner owner, Lifecycle.Event event) {
- mCollectedEvents.add(event);
- }
-
- List<Lifecycle.Event> getEventsAndReset() {
- ArrayList<Lifecycle.Event> events = new ArrayList<>(mCollectedEvents);
- mCollectedEvents.clear();
- return events;
- }
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ServiceLifecycleTest.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ServiceLifecycleTest.java
deleted file mode 100644
index fe0a306..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ServiceLifecycleTest.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * 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_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.service.TestService;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.os.IBinder;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v4.content.LocalBroadcastManager;
-
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class ServiceLifecycleTest {
-
- private static final int RETRY_NUMBER = 5;
- private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(1);
-
- private Intent mServiceIntent;
-
- private volatile List<Event> mLoggerEvents;
- private EventLogger mLogger;
-
- @Before
- public void setUp() {
- Context context = InstrumentationRegistry.getTargetContext();
- mServiceIntent = new Intent(context, TestService.class);
- LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(TestService.ACTION_LOG_EVENT);
-
- // Overcautiousness: each EventLogger has its own events list, so one bad test won't spoil
- // others.
- mLoggerEvents = new ArrayList<>();
- mLogger = new EventLogger(mLoggerEvents);
- localBroadcastManager.registerReceiver(mLogger, intentFilter);
-
- }
-
- @After
- public void tearDown() {
- Context context = InstrumentationRegistry.getTargetContext();
- LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
- localBroadcastManager.unregisterReceiver(mLogger);
- mLogger = null;
- mLoggerEvents = null;
- }
-
- @Test
- public void testUnboundedService() throws TimeoutException, InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- context.startService(mServiceIntent);
- awaitAndAssertEvents(ON_CREATE, ON_START);
- context.stopService(mServiceIntent);
- awaitAndAssertEvents(ON_CREATE, ON_START, ON_STOP, ON_DESTROY);
- }
-
- @Test
- public void testBoundedService() throws TimeoutException, InterruptedException {
- ServiceConnection connection = bindToService();
- awaitAndAssertEvents(ON_CREATE, ON_START);
- InstrumentationRegistry.getTargetContext().unbindService(connection);
- awaitAndAssertEvents(ON_CREATE, ON_START, ON_STOP, ON_DESTROY);
- }
-
- @Test
- public void testStartBindUnbindStop() throws InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- context.startService(mServiceIntent);
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- ServiceConnection connection = bindToService();
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // still the same events
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.unbindService(connection);
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // service is still started (stopServices/stopSelf weren't called)
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.stopService(mServiceIntent);
- awaitAndAssertEvents(ON_CREATE, ON_START, ON_STOP, ON_DESTROY);
- }
-
- @Test
- public void testStartBindStopUnbind() throws InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- context.startService(mServiceIntent);
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- ServiceConnection connection = bindToService();
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // still the same events
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.stopService(mServiceIntent);
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // service is still bound
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.unbindService(connection);
- awaitAndAssertEvents(ON_CREATE, ON_START,
- ON_STOP, ON_DESTROY);
- }
-
- @Test
- public void testBindStartUnbindStop() throws InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- ServiceConnection connection = bindToService();
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
-
- context.startService(mServiceIntent);
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // still the same events
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.unbindService(connection);
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // service is still started (stopServices/stopSelf weren't called)
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.stopService(mServiceIntent);
- awaitAndAssertEvents(ON_CREATE, ON_START,
- ON_STOP, ON_DESTROY);
- }
-
- @Test
- public void testBindStartStopUnbind() throws InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- ServiceConnection connection = bindToService();
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.startService(mServiceIntent);
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // still the same events
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.stopService(mServiceIntent);
- // Precaution: give a chance to dispatch events
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- // service is still bound
- awaitAndAssertEvents(ON_CREATE, ON_START);
-
- context.unbindService(connection);
- awaitAndAssertEvents(ON_CREATE, ON_START,
- ON_STOP, ON_DESTROY);
- }
-
- // can't use ServiceTestRule because it proxies connection, so we can't use unbindService method
- private ServiceConnection bindToService() throws InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- final CountDownLatch latch = new CountDownLatch(1);
- ServiceConnection connection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- latch.countDown();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
-
- }
- };
-
- boolean success = context.bindService(mServiceIntent, connection, Context.BIND_AUTO_CREATE);
- assertThat(success, is(true));
- boolean awaited = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
- assertThat(awaited, is(true));
- return connection;
- }
-
- private void awaitAndAssertEvents(Event... events) throws InterruptedException {
- //noinspection SynchronizeOnNonFinalField
- synchronized (mLoggerEvents) {
- int retryCount = 0;
- while (mLoggerEvents.size() < events.length && retryCount++ < RETRY_NUMBER) {
- mLoggerEvents.wait(TIMEOUT);
- }
- assertThat(mLoggerEvents, is(Arrays.asList(events)));
- }
- }
-
- private static class EventLogger extends BroadcastReceiver {
- private final List<Event> mLoggerEvents;
-
- private EventLogger(List<Event> loggerEvents) {
- mLoggerEvents = loggerEvents;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- synchronized (mLoggerEvents) {
- mLoggerEvents.add((Event) intent.getSerializableExtra(TestService.EXTRA_KEY_EVENT));
- mLoggerEvents.notifyAll();
- }
- }
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ViewModelTest.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ViewModelTest.java
deleted file mode 100644
index 03ebdf3..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ViewModelTest.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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_RESUME;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.app.Instrumentation;
-import android.arch.lifecycle.viewmodeltest.TestViewModel;
-import android.arch.lifecycle.viewmodeltest.ViewModelActivity;
-import android.arch.lifecycle.viewmodeltest.ViewModelActivity.ViewModelFragment;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.annotation.UiThreadTest;
-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;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class ViewModelTest {
- private static final int TIMEOUT = 2; // secs
-
- @Rule
- public ActivityTestRule<ViewModelActivity> mActivityRule =
- new ActivityTestRule<>(ViewModelActivity.class);
-
- @Test
- public void ensureSameViewHolders() throws Throwable {
- final TestViewModel[] activityModel = new TestViewModel[1];
- final TestViewModel[] defaultActivityModel = new TestViewModel[1];
- final TestViewModel[] fragment1Model = new TestViewModel[1];
- final TestViewModel[] fragment2Model = new TestViewModel[1];
- final ViewModelActivity[] viewModelActivity = new ViewModelActivity[1];
- viewModelActivity[0] = mActivityRule.getActivity();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- ViewModelFragment fragment1 = getFragment(viewModelActivity[0],
- ViewModelActivity.FRAGMENT_TAG_1);
- ViewModelFragment fragment2 = getFragment(viewModelActivity[0],
- ViewModelActivity.FRAGMENT_TAG_2);
- assertThat(fragment1, notNullValue());
- assertThat(fragment2, notNullValue());
- assertThat(fragment1.activityModel, is(fragment2.activityModel));
- assertThat(fragment1.fragmentModel, not(is(fragment2.activityModel)));
- assertThat(mActivityRule.getActivity().activityModel, is(fragment1.activityModel));
- activityModel[0] = mActivityRule.getActivity().activityModel;
- defaultActivityModel[0] = mActivityRule.getActivity().defaultActivityModel;
- assertThat(defaultActivityModel[0], not(is(activityModel[0])));
- fragment1Model[0] = fragment1.fragmentModel;
- fragment2Model[0] = fragment2.fragmentModel;
- }
- });
- viewModelActivity[0] = recreateActivity();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- ViewModelFragment fragment1 = getFragment(viewModelActivity[0],
- ViewModelActivity.FRAGMENT_TAG_1);
- ViewModelFragment fragment2 = getFragment(viewModelActivity[0],
- ViewModelActivity.FRAGMENT_TAG_2);
- assertThat(fragment1, notNullValue());
- assertThat(fragment2, notNullValue());
-
- assertThat(fragment1.activityModel, is(activityModel[0]));
- assertThat(fragment2.activityModel, is(activityModel[0]));
- assertThat(fragment1.fragmentModel, is(fragment1Model[0]));
- assertThat(fragment2.fragmentModel, is(fragment2Model[0]));
- assertThat(fragment1.defaultActivityModel, is(defaultActivityModel[0]));
- assertThat(fragment2.defaultActivityModel, is(defaultActivityModel[0]));
- assertThat(mActivityRule.getActivity().activityModel, is(activityModel[0]));
- assertThat(mActivityRule.getActivity().defaultActivityModel,
- is(defaultActivityModel[0]));
- }
- });
- }
-
- @Test
- @UiThreadTest
- public void testGetApplication() {
- TestViewModel activityModel = mActivityRule.getActivity().activityModel;
- assertThat(activityModel.getApplication(),
- is(InstrumentationRegistry.getTargetContext().getApplicationContext()));
- }
-
- @Test
- public void testOnClear() throws Throwable {
- final ViewModelActivity activity = mActivityRule.getActivity();
- final CountDownLatch latch = new CountDownLatch(1);
- final LifecycleObserver observer = new LifecycleObserver() {
- @SuppressWarnings("unused")
- @OnLifecycleEvent(ON_RESUME)
- void onResume() {
- try {
- final FragmentManager manager = activity.getSupportFragmentManager();
- Fragment fragment = new Fragment();
- manager.beginTransaction().add(fragment, "temp").commitNow();
- ViewModel1 vm = ViewModelProviders.of(fragment).get(ViewModel1.class);
- assertThat(vm.mCleared, is(false));
- manager.beginTransaction().remove(fragment).commitNow();
- assertThat(vm.mCleared, is(true));
- } finally {
- latch.countDown();
- }
- }
- };
-
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- activity.getLifecycle().addObserver(observer);
- }
- });
- assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
- }
-
- private ViewModelFragment getFragment(FragmentActivity activity, String tag) {
- return (ViewModelFragment) activity.getSupportFragmentManager()
- .findFragmentByTag(tag);
- }
-
- private ViewModelActivity recreateActivity() throws Throwable {
- Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
- ViewModelActivity.class.getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
- final ViewModelActivity previous = mActivityRule.getActivity();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- previous.recreate();
- }
- });
- ViewModelActivity result;
-
- // this guarantee that we will reinstall monitor between notifications about onDestroy
- // and onCreate
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
- synchronized (monitor) {
- do {
- // the documentation says "Block until an Activity is created
- // that matches this monitor." This statement is true, but there are some other
- // true statements like: "Block until an Activity is destroyed" or
- // "Block until an Activity is resumed"...
-
- // this call will release synchronization monitor's monitor
- result = (ViewModelActivity) monitor.waitForActivityWithTimeout(4000);
- if (result == null) {
- throw new RuntimeException("Timeout. Failed to recreate an activity");
- }
- } while (result == previous);
- }
- return result;
- }
-
- public static class ViewModel1 extends ViewModel {
- boolean mCleared = false;
-
- @Override
- protected void onCleared() {
- mCleared = true;
- }
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ViewModelTestInTransaction.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ViewModelTestInTransaction.java
deleted file mode 100644
index 3832b3b..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/ViewModelTestInTransaction.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.test.annotation.UiThreadTest;
-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.arch.lifecycle.viewmodeltest.TestViewModel;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class ViewModelTestInTransaction {
-
- @Rule
- public ActivityTestRule<EmptyActivity> mActivityRule =
- new ActivityTestRule<>(EmptyActivity.class);
-
- @Test
- @UiThreadTest
- public void testViewModelInTransactionActivity() {
- EmptyActivity activity = mActivityRule.getActivity();
- TestFragment fragment = new TestFragment();
- activity.getSupportFragmentManager().beginTransaction().add(fragment, "tag").commitNow();
- TestViewModel viewModel = ViewModelProviders.of(activity).get(TestViewModel.class);
- assertThat(viewModel, is(fragment.mViewModel));
- }
-
- @Test
- @UiThreadTest
- public void testViewModelInTransactionFragment() {
- EmptyActivity activity = mActivityRule.getActivity();
- ParentFragment parent = new ParentFragment();
- activity.getSupportFragmentManager().beginTransaction().add(parent, "parent").commitNow();
- assertThat(parent.mExecuted, is(true));
- }
-
-
- public static class ParentFragment extends Fragment {
-
- private boolean mExecuted;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- TestFragment fragment = new TestFragment();
- getChildFragmentManager().beginTransaction().add(fragment, "tag").commitNow();
- TestViewModel viewModel = ViewModelProviders.of(this).get(TestViewModel.class);
- assertThat(viewModel, is(fragment.mViewModel));
- mExecuted = true;
- }
- }
-
- public static class TestFragment extends Fragment {
-
- TestViewModel mViewModel;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Fragment parentFragment = getParentFragment();
- ViewModelProvider provider = parentFragment != null
- ? ViewModelProviders.of(parentFragment) : ViewModelProviders.of(getActivity());
- mViewModel = provider.get(TestViewModel.class);
- assertThat(mViewModel, notNullValue());
- }
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/activity/EmptyActivity.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/activity/EmptyActivity.java
deleted file mode 100644
index c32c898..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/activity/EmptyActivity.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.activity;
-
-import android.arch.lifecycle.extensions.test.R;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.app.FragmentActivity;
-
-public class EmptyActivity extends FragmentActivity {
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/activity/FragmentLifecycleActivity.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/activity/FragmentLifecycleActivity.java
deleted file mode 100644
index 2eb1cc2..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/activity/FragmentLifecycleActivity.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2016 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.activity;
-
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
-import android.arch.lifecycle.extensions.test.R;
-import android.content.Context;
-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;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-public class FragmentLifecycleActivity extends AppCompatActivity {
- public static final String NESTED_TAG = "nested_fragment";
- public static final String MAIN_TAG = "main_fragment";
- private static final String EXTRA_NESTED = "nested";
-
- private final List<Lifecycle.Event> mLoggedEvents = Collections
- .synchronizedList(new ArrayList<Lifecycle.Event>());
- private LifecycleOwner mObservedOwner;
- private final CountDownLatch mDestroyLatch = new CountDownLatch(1);
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- MainFragment fragment;
- fragment = new MainFragment();
- boolean nested = getIntent().getBooleanExtra(EXTRA_NESTED, false);
- if (nested) {
- fragment.mNestedFragment = new NestedFragment();
- }
- observe(nested ? fragment.mNestedFragment : fragment);
- getSupportFragmentManager().beginTransaction()
- .add(R.id.fragment_container, fragment, MAIN_TAG)
- .commit();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mDestroyLatch.countDown();
- }
-
- public void resetEvents() {
- mLoggedEvents.clear();
- }
-
- public static class MainFragment extends Fragment {
- @Nullable
- Fragment mNestedFragment;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (mNestedFragment != null) {
- getChildFragmentManager().beginTransaction()
- .add(mNestedFragment, NESTED_TAG)
- .commit();
- }
- }
- }
-
- public static class NestedFragment extends Fragment {
- }
-
- public static Intent intentFor(Context context, boolean nested) {
- Intent intent = new Intent(context, FragmentLifecycleActivity.class);
- intent.putExtra(EXTRA_NESTED, nested);
- return intent;
- }
-
- public void observe(LifecycleOwner provider) {
- mObservedOwner = provider;
- provider.getLifecycle().addObserver(new LifecycleObserver() {
- @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- public void anyEvent(@SuppressWarnings("unused") LifecycleOwner owner,
- Lifecycle.Event event) {
- mLoggedEvents.add(event);
- }
- });
- }
-
- public List<Lifecycle.Event> getLoggedEvents() {
- return mLoggedEvents;
- }
-
- public LifecycleOwner getObservedOwner() {
- return mObservedOwner;
- }
-
- public boolean awaitForDestruction(long timeout, TimeUnit timeUnit)
- throws InterruptedException {
- return mDestroyLatch.await(timeout, timeUnit);
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/service/TestService.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/service/TestService.java
deleted file mode 100644
index fcf06cc..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/service/TestService.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2016 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.service;
-
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.LifecycleService;
-import android.arch.lifecycle.OnLifecycleEvent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.IBinder;
-import android.support.annotation.Nullable;
-import android.support.v4.content.LocalBroadcastManager;
-
-public class TestService extends LifecycleService {
-
- public static final String ACTION_LOG_EVENT = "ACTION_LOG_EVENT";
- public static final String EXTRA_KEY_EVENT = "EXTRA_KEY_EVENT";
-
- private final IBinder mBinder = new Binder();
-
- public TestService() {
- getLifecycle().addObserver(new LifecycleObserver() {
- @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- public void anyEvent(LifecycleOwner owner, Lifecycle.Event event) {
- Context context = (TestService) owner;
- Intent intent = new Intent(ACTION_LOG_EVENT);
- intent.putExtra(EXTRA_KEY_EVENT, event);
- LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
- }
- });
- }
-
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- super.onBind(intent);
- return mBinder;
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/viewmodeltest/TestViewModel.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/viewmodeltest/TestViewModel.java
deleted file mode 100644
index ec3550c..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/viewmodeltest/TestViewModel.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2016 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.viewmodeltest;
-
-import android.app.Application;
-
-import android.arch.lifecycle.AndroidViewModel;
-
-public class TestViewModel extends AndroidViewModel {
-
- public TestViewModel(Application application) {
- super(application);
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java b/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java
deleted file mode 100644
index 1f9f100..0000000
--- a/lifecycle/extensions/src/androidTest/java/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2016 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.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;
-
-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";
- public static final String FRAGMENT_TAG_2 = "f2";
-
- public TestViewModel activityModel;
- public TestViewModel defaultActivityModel;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_view_model);
- if (savedInstanceState == null) {
- getSupportFragmentManager().beginTransaction()
- .add(R.id.fragment_container, new ViewModelFragment(), FRAGMENT_TAG_1)
- .add(new ViewModelFragment(), FRAGMENT_TAG_2)
- .commit();
- }
- activityModel = ViewModelProviders.of(this).get(KEY_ACTIVITY_MODEL, TestViewModel.class);
- defaultActivityModel = ViewModelProviders.of(this).get(TestViewModel.class);
- }
-
- public static class ViewModelFragment extends Fragment {
- public TestViewModel fragmentModel;
- public TestViewModel activityModel;
- public TestViewModel defaultActivityModel;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- fragmentModel = ViewModelProviders.of(this).get(KEY_FRAGMENT_MODEL,
- TestViewModel.class);
- activityModel = ViewModelProviders.of(getActivity()).get(KEY_ACTIVITY_MODEL,
- TestViewModel.class);
- defaultActivityModel = ViewModelProviders.of(getActivity()).get(TestViewModel.class);
- }
- }
-}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/EmptyActivity.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/EmptyActivity.java
new file mode 100644
index 0000000..33fd260
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/EmptyActivity.java
@@ -0,0 +1,22 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.fragment.app.FragmentActivity;
+
+public class EmptyActivity extends FragmentActivity {
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/FragmentLifecycleInActivityTest.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/FragmentLifecycleInActivityTest.java
new file mode 100644
index 0000000..40c7225
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/FragmentLifecycleInActivityTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+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.app.Instrumentation;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.activity.FragmentLifecycleActivity;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(Parameterized.class)
+public class FragmentLifecycleInActivityTest {
+
+ private static final long TIMEOUT = 2; //sec
+
+ @Rule
+ public ActivityTestRule<FragmentLifecycleActivity> mActivityRule =
+ new ActivityTestRule<>(FragmentLifecycleActivity.class, false, false);
+
+ private Instrumentation mInstrumentation;
+
+ @SuppressWarnings("WeakerAccess")
+ @Parameterized.Parameter
+ public boolean mNested;
+
+ @Parameterized.Parameters(name = "nested_{0}")
+ public static Object[][] params() {
+ return new Object[][]{new Object[]{false}, new Object[]{true}};
+ }
+
+ @Before
+ public void getInstrumentation() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ private void reset() {
+ mActivityRule.getActivity().resetEvents();
+ }
+
+ @Test
+ public void testFullEvents() throws Throwable {
+ final FragmentLifecycleActivity activity = launchActivity();
+ waitForIdle();
+ assertEvents(ON_CREATE, ON_START, ON_RESUME);
+ reset();
+ finishActivity(activity);
+ assertEvents(ON_PAUSE, ON_STOP, ON_DESTROY);
+ }
+
+ @Test
+ public void testStopStart() throws Throwable {
+ final FragmentLifecycleActivity activity = launchActivity();
+ waitForIdle();
+ assertEvents(ON_CREATE, ON_START, ON_RESUME);
+ reset();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mInstrumentation.callActivityOnPause(activity);
+ mInstrumentation.callActivityOnStop(activity);
+ }
+ });
+ waitForIdle();
+ assertEvents(ON_PAUSE, ON_STOP);
+ reset();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mInstrumentation.callActivityOnStart(activity);
+ mInstrumentation.callActivityOnResume(activity);
+ }
+ });
+ waitForIdle();
+ assertEvents(ON_START, ON_RESUME);
+ }
+
+ private FragmentLifecycleActivity launchActivity() throws Throwable {
+ Intent intent = FragmentLifecycleActivity.intentFor(mInstrumentation.getTargetContext(),
+ mNested);
+ final FragmentLifecycleActivity activity = mActivityRule.launchActivity(intent);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Fragment main = activity.getSupportFragmentManager()
+ .findFragmentByTag(FragmentLifecycleActivity.MAIN_TAG);
+ assertThat("test sanity", main, notNullValue());
+ Fragment nestedFragment = main.getChildFragmentManager()
+ .findFragmentByTag(FragmentLifecycleActivity.NESTED_TAG);
+ assertThat("test sanity", nestedFragment != null, is(mNested));
+ }
+ });
+ assertThat(activity.getObservedOwner(), instanceOf(
+ mNested ? FragmentLifecycleActivity.NestedFragment.class
+ : FragmentLifecycleActivity.MainFragment.class
+ ));
+ return activity;
+ }
+
+ private void waitForIdle() {
+ mInstrumentation.waitForIdleSync();
+ }
+
+ private void finishActivity(final FragmentLifecycleActivity activity)
+ throws InterruptedException {
+ mInstrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ activity.finish();
+ }
+ });
+ assertThat(activity.awaitForDestruction(TIMEOUT, TimeUnit.SECONDS), is(true));
+ }
+
+ private void assertEvents(Lifecycle.Event... events) {
+ assertThat(mActivityRule.getActivity().getLoggedEvents(), is(Arrays.asList(events)));
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/FragmentOperationsLifecycleTest.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/FragmentOperationsLifecycleTest.java
new file mode 100644
index 0000000..0e30381
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/FragmentOperationsLifecycleTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import static java.util.Arrays.asList;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.activity.EmptyActivity;
+import androidx.lifecycle.extensions.test.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class FragmentOperationsLifecycleTest {
+
+ @Rule
+ public ActivityTestRule<EmptyActivity> mActivityTestRule = new ActivityTestRule<>(
+ EmptyActivity.class);
+
+ @Test
+ @UiThreadTest
+ public void addRemoveFragment() {
+ EmptyActivity activity = mActivityTestRule.getActivity();
+ Fragment fragment = new Fragment();
+ FragmentManager fm = activity.getSupportFragmentManager();
+ fm.beginTransaction().add(fragment, "tag").commitNow();
+ CollectingObserver observer = observeAndCollectIn(fragment);
+ assertThat(observer.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
+ fm.beginTransaction().remove(fragment).commitNow();
+ assertThat(observer.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
+ fm.beginTransaction().add(fragment, "tag").commitNow();
+ assertThat(observer.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
+ }
+
+ @Test
+ @UiThreadTest
+ public void fragmentInBackstack() {
+ EmptyActivity activity = mActivityTestRule.getActivity();
+ Fragment fragment1 = new Fragment();
+ FragmentManager fm = activity.getSupportFragmentManager();
+ fm.beginTransaction().add(R.id.fragment_container, fragment1, "tag").addToBackStack(null)
+ .commit();
+ fm.executePendingTransactions();
+ CollectingObserver observer1 = observeAndCollectIn(fragment1);
+ assertThat(observer1.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
+
+ Fragment fragment2 = new Fragment();
+ fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null)
+ .commit();
+ fm.executePendingTransactions();
+
+ CollectingObserver observer2 = observeAndCollectIn(fragment2);
+ assertThat(observer1.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP)));
+ assertThat(observer2.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME)));
+
+ assertThat(fm.popBackStackImmediate(), is(true));
+ assertThat(observer1.getEventsAndReset(), is(asList(ON_START, ON_RESUME)));
+ assertThat(observer2.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
+
+ assertThat(fm.popBackStackImmediate(), is(true));
+ assertThat(observer1.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY)));
+ }
+
+ private static CollectingObserver observeAndCollectIn(Fragment fragment) {
+ CollectingObserver observer = new CollectingObserver();
+ fragment.getLifecycle().addObserver(observer);
+ return observer;
+ }
+
+ private static class CollectingObserver implements LifecycleObserver {
+ final List<Lifecycle.Event> mCollectedEvents = new ArrayList<>();
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
+ public void anyEvent(LifecycleOwner owner, Lifecycle.Event event) {
+ mCollectedEvents.add(event);
+ }
+
+ List<Lifecycle.Event> getEventsAndReset() {
+ ArrayList<Lifecycle.Event> events = new ArrayList<>(mCollectedEvents);
+ mCollectedEvents.clear();
+ return events;
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
new file mode 100644
index 0000000..7f58907
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.service.TestService;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ServiceLifecycleTest {
+
+ private static final int RETRY_NUMBER = 5;
+ private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+
+ private Intent mServiceIntent;
+
+ private volatile List<Event> mLoggerEvents;
+ private EventLogger mLogger;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ mServiceIntent = new Intent(context, TestService.class);
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(TestService.ACTION_LOG_EVENT);
+
+ // Overcautiousness: each EventLogger has its own events list, so one bad test won't spoil
+ // others.
+ mLoggerEvents = new ArrayList<>();
+ mLogger = new EventLogger(mLoggerEvents);
+ localBroadcastManager.registerReceiver(mLogger, intentFilter);
+
+ }
+
+ @After
+ public void tearDown() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
+ localBroadcastManager.unregisterReceiver(mLogger);
+ mLogger = null;
+ mLoggerEvents = null;
+ }
+
+ @Test
+ public void testUnboundedService() throws TimeoutException, InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ context.startService(mServiceIntent);
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+ context.stopService(mServiceIntent);
+ awaitAndAssertEvents(ON_CREATE, ON_START, ON_STOP, ON_DESTROY);
+ }
+
+ @Test
+ public void testBoundedService() throws TimeoutException, InterruptedException {
+ ServiceConnection connection = bindToService();
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+ InstrumentationRegistry.getTargetContext().unbindService(connection);
+ awaitAndAssertEvents(ON_CREATE, ON_START, ON_STOP, ON_DESTROY);
+ }
+
+ @Test
+ public void testStartBindUnbindStop() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ context.startService(mServiceIntent);
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ ServiceConnection connection = bindToService();
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // still the same events
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.unbindService(connection);
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // service is still started (stopServices/stopSelf weren't called)
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.stopService(mServiceIntent);
+ awaitAndAssertEvents(ON_CREATE, ON_START, ON_STOP, ON_DESTROY);
+ }
+
+ @Test
+ public void testStartBindStopUnbind() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ context.startService(mServiceIntent);
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ ServiceConnection connection = bindToService();
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // still the same events
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.stopService(mServiceIntent);
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // service is still bound
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.unbindService(connection);
+ awaitAndAssertEvents(ON_CREATE, ON_START,
+ ON_STOP, ON_DESTROY);
+ }
+
+ @Test
+ public void testBindStartUnbindStop() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ ServiceConnection connection = bindToService();
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+
+ context.startService(mServiceIntent);
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // still the same events
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.unbindService(connection);
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // service is still started (stopServices/stopSelf weren't called)
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.stopService(mServiceIntent);
+ awaitAndAssertEvents(ON_CREATE, ON_START,
+ ON_STOP, ON_DESTROY);
+ }
+
+ @Test
+ public void testBindStartStopUnbind() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ ServiceConnection connection = bindToService();
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.startService(mServiceIntent);
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // still the same events
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.stopService(mServiceIntent);
+ // Precaution: give a chance to dispatch events
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // service is still bound
+ awaitAndAssertEvents(ON_CREATE, ON_START);
+
+ context.unbindService(connection);
+ awaitAndAssertEvents(ON_CREATE, ON_START,
+ ON_STOP, ON_DESTROY);
+ }
+
+ // can't use ServiceTestRule because it proxies connection, so we can't use unbindService method
+ private ServiceConnection bindToService() throws InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ final CountDownLatch latch = new CountDownLatch(1);
+ ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ latch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+
+ }
+ };
+
+ boolean success = context.bindService(mServiceIntent, connection, Context.BIND_AUTO_CREATE);
+ assertThat(success, is(true));
+ boolean awaited = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
+ assertThat(awaited, is(true));
+ return connection;
+ }
+
+ private void awaitAndAssertEvents(Event... events) throws InterruptedException {
+ //noinspection SynchronizeOnNonFinalField
+ synchronized (mLoggerEvents) {
+ int retryCount = 0;
+ while (mLoggerEvents.size() < events.length && retryCount++ < RETRY_NUMBER) {
+ mLoggerEvents.wait(TIMEOUT);
+ }
+ assertThat(mLoggerEvents, is(Arrays.asList(events)));
+ }
+ }
+
+ private static class EventLogger extends BroadcastReceiver {
+ private final List<Event> mLoggerEvents;
+
+ private EventLogger(List<Event> loggerEvents) {
+ mLoggerEvents = loggerEvents;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mLoggerEvents) {
+ mLoggerEvents.add((Event) intent.getSerializableExtra(TestService.EXTRA_KEY_EVENT));
+ mLoggerEvents.notifyAll();
+ }
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ViewModelTest.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ViewModelTest.java
new file mode 100644
index 0000000..89d32ff
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ViewModelTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.app.Instrumentation;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.viewmodeltest.TestViewModel;
+import androidx.lifecycle.viewmodeltest.ViewModelActivity;
+import androidx.lifecycle.viewmodeltest.ViewModelActivity.ViewModelFragment;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ViewModelTest {
+ private static final int TIMEOUT = 2; // secs
+
+ @Rule
+ public ActivityTestRule<ViewModelActivity> mActivityRule =
+ new ActivityTestRule<>(ViewModelActivity.class);
+
+ @Test
+ public void ensureSameViewHolders() throws Throwable {
+ final TestViewModel[] activityModel = new TestViewModel[1];
+ final TestViewModel[] defaultActivityModel = new TestViewModel[1];
+ final TestViewModel[] fragment1Model = new TestViewModel[1];
+ final TestViewModel[] fragment2Model = new TestViewModel[1];
+ final ViewModelActivity[] viewModelActivity = new ViewModelActivity[1];
+ viewModelActivity[0] = mActivityRule.getActivity();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ViewModelFragment fragment1 = getFragment(viewModelActivity[0],
+ ViewModelActivity.FRAGMENT_TAG_1);
+ ViewModelFragment fragment2 = getFragment(viewModelActivity[0],
+ ViewModelActivity.FRAGMENT_TAG_2);
+ assertThat(fragment1, notNullValue());
+ assertThat(fragment2, notNullValue());
+ assertThat(fragment1.activityModel, is(fragment2.activityModel));
+ assertThat(fragment1.fragmentModel, not(is(fragment2.activityModel)));
+ assertThat(mActivityRule.getActivity().activityModel, is(fragment1.activityModel));
+ activityModel[0] = mActivityRule.getActivity().activityModel;
+ defaultActivityModel[0] = mActivityRule.getActivity().defaultActivityModel;
+ assertThat(defaultActivityModel[0], not(is(activityModel[0])));
+ fragment1Model[0] = fragment1.fragmentModel;
+ fragment2Model[0] = fragment2.fragmentModel;
+ }
+ });
+ viewModelActivity[0] = recreateActivity();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ViewModelFragment fragment1 = getFragment(viewModelActivity[0],
+ ViewModelActivity.FRAGMENT_TAG_1);
+ ViewModelFragment fragment2 = getFragment(viewModelActivity[0],
+ ViewModelActivity.FRAGMENT_TAG_2);
+ assertThat(fragment1, notNullValue());
+ assertThat(fragment2, notNullValue());
+
+ assertThat(fragment1.activityModel, is(activityModel[0]));
+ assertThat(fragment2.activityModel, is(activityModel[0]));
+ assertThat(fragment1.fragmentModel, is(fragment1Model[0]));
+ assertThat(fragment2.fragmentModel, is(fragment2Model[0]));
+ assertThat(fragment1.defaultActivityModel, is(defaultActivityModel[0]));
+ assertThat(fragment2.defaultActivityModel, is(defaultActivityModel[0]));
+ assertThat(mActivityRule.getActivity().activityModel, is(activityModel[0]));
+ assertThat(mActivityRule.getActivity().defaultActivityModel,
+ is(defaultActivityModel[0]));
+ }
+ });
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGetApplication() {
+ TestViewModel activityModel = mActivityRule.getActivity().activityModel;
+ assertThat(activityModel.getApplication(),
+ is(InstrumentationRegistry.getTargetContext().getApplicationContext()));
+ }
+
+ @Test
+ public void testOnClear() throws Throwable {
+ final ViewModelActivity activity = mActivityRule.getActivity();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final LifecycleObserver observer = new LifecycleObserver() {
+ @SuppressWarnings("unused")
+ @OnLifecycleEvent(ON_RESUME)
+ void onResume() {
+ try {
+ final FragmentManager manager = activity.getSupportFragmentManager();
+ Fragment fragment = new Fragment();
+ manager.beginTransaction().add(fragment, "temp").commitNow();
+ ViewModel1 vm = ViewModelProviders.of(fragment).get(ViewModel1.class);
+ assertThat(vm.mCleared, is(false));
+ manager.beginTransaction().remove(fragment).commitNow();
+ assertThat(vm.mCleared, is(true));
+ } finally {
+ latch.countDown();
+ }
+ }
+ };
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.getLifecycle().addObserver(observer);
+ }
+ });
+ assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
+ }
+
+ private ViewModelFragment getFragment(FragmentActivity activity, String tag) {
+ return (ViewModelFragment) activity.getSupportFragmentManager()
+ .findFragmentByTag(tag);
+ }
+
+ private ViewModelActivity recreateActivity() throws Throwable {
+ Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+ ViewModelActivity.class.getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+ final ViewModelActivity previous = mActivityRule.getActivity();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ previous.recreate();
+ }
+ });
+ ViewModelActivity result;
+
+ // this guarantee that we will reinstall monitor between notifications about onDestroy
+ // and onCreate
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (monitor) {
+ do {
+ // the documentation says "Block until an Activity is created
+ // that matches this monitor." This statement is true, but there are some other
+ // true statements like: "Block until an Activity is destroyed" or
+ // "Block until an Activity is resumed"...
+
+ // this call will release synchronization monitor's monitor
+ result = (ViewModelActivity) monitor.waitForActivityWithTimeout(4000);
+ if (result == null) {
+ throw new RuntimeException("Timeout. Failed to recreate an activity");
+ }
+ } while (result == previous);
+ }
+ return result;
+ }
+
+ public static class ViewModel1 extends ViewModel {
+ boolean mCleared = false;
+
+ @Override
+ protected void onCleared() {
+ mCleared = true;
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ViewModelTestInTransaction.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ViewModelTestInTransaction.java
new file mode 100644
index 0000000..091bede
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/ViewModelTestInTransaction.java
@@ -0,0 +1,94 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.os.Bundle;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.viewmodeltest.TestViewModel;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ViewModelTestInTransaction {
+
+ @Rule
+ public ActivityTestRule<EmptyActivity> mActivityRule =
+ new ActivityTestRule<>(EmptyActivity.class);
+
+ @Test
+ @UiThreadTest
+ public void testViewModelInTransactionActivity() {
+ EmptyActivity activity = mActivityRule.getActivity();
+ TestFragment fragment = new TestFragment();
+ activity.getSupportFragmentManager().beginTransaction().add(fragment, "tag").commitNow();
+ TestViewModel viewModel = ViewModelProviders.of(activity).get(TestViewModel.class);
+ assertThat(viewModel, is(fragment.mViewModel));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testViewModelInTransactionFragment() {
+ EmptyActivity activity = mActivityRule.getActivity();
+ ParentFragment parent = new ParentFragment();
+ activity.getSupportFragmentManager().beginTransaction().add(parent, "parent").commitNow();
+ assertThat(parent.mExecuted, is(true));
+ }
+
+
+ public static class ParentFragment extends Fragment {
+
+ private boolean mExecuted;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TestFragment fragment = new TestFragment();
+ getChildFragmentManager().beginTransaction().add(fragment, "tag").commitNow();
+ TestViewModel viewModel = ViewModelProviders.of(this).get(TestViewModel.class);
+ assertThat(viewModel, is(fragment.mViewModel));
+ mExecuted = true;
+ }
+ }
+
+ public static class TestFragment extends Fragment {
+
+ TestViewModel mViewModel;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Fragment parentFragment = getParentFragment();
+ ViewModelProvider provider = parentFragment != null
+ ? ViewModelProviders.of(parentFragment) : ViewModelProviders.of(getActivity());
+ mViewModel = provider.get(TestViewModel.class);
+ assertThat(mViewModel, notNullValue());
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/activity/EmptyActivity.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/activity/EmptyActivity.java
new file mode 100644
index 0000000..66bf9eb
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/activity/EmptyActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.lifecycle.activity;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.extensions.test.R;
+
+public class EmptyActivity extends FragmentActivity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/activity/FragmentLifecycleActivity.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/activity/FragmentLifecycleActivity.java
new file mode 100644
index 0000000..597111d
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/activity/FragmentLifecycleActivity.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.extensions.test.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class FragmentLifecycleActivity extends AppCompatActivity {
+ public static final String NESTED_TAG = "nested_fragment";
+ public static final String MAIN_TAG = "main_fragment";
+ private static final String EXTRA_NESTED = "nested";
+
+ private final List<Lifecycle.Event> mLoggedEvents = Collections
+ .synchronizedList(new ArrayList<Lifecycle.Event>());
+ private LifecycleOwner mObservedOwner;
+ private final CountDownLatch mDestroyLatch = new CountDownLatch(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ MainFragment fragment;
+ fragment = new MainFragment();
+ boolean nested = getIntent().getBooleanExtra(EXTRA_NESTED, false);
+ if (nested) {
+ fragment.mNestedFragment = new NestedFragment();
+ }
+ observe(nested ? fragment.mNestedFragment : fragment);
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.fragment_container, fragment, MAIN_TAG)
+ .commit();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mDestroyLatch.countDown();
+ }
+
+ public void resetEvents() {
+ mLoggedEvents.clear();
+ }
+
+ public static class MainFragment extends Fragment {
+ @Nullable
+ Fragment mNestedFragment;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (mNestedFragment != null) {
+ getChildFragmentManager().beginTransaction()
+ .add(mNestedFragment, NESTED_TAG)
+ .commit();
+ }
+ }
+ }
+
+ public static class NestedFragment extends Fragment {
+ }
+
+ public static Intent intentFor(Context context, boolean nested) {
+ Intent intent = new Intent(context, FragmentLifecycleActivity.class);
+ intent.putExtra(EXTRA_NESTED, nested);
+ return intent;
+ }
+
+ public void observe(LifecycleOwner provider) {
+ mObservedOwner = provider;
+ provider.getLifecycle().addObserver(new LifecycleObserver() {
+ @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
+ public void anyEvent(@SuppressWarnings("unused") LifecycleOwner owner,
+ Lifecycle.Event event) {
+ mLoggedEvents.add(event);
+ }
+ });
+ }
+
+ public List<Lifecycle.Event> getLoggedEvents() {
+ return mLoggedEvents;
+ }
+
+ public LifecycleOwner getObservedOwner() {
+ return mObservedOwner;
+ }
+
+ public boolean awaitForDestruction(long timeout, TimeUnit timeUnit)
+ throws InterruptedException {
+ return mDestroyLatch.await(timeout, timeUnit);
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/service/TestService.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/service/TestService.java
new file mode 100644
index 0000000..afc9dc8
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/service/TestService.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleService;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+public class TestService extends LifecycleService {
+
+ public static final String ACTION_LOG_EVENT = "ACTION_LOG_EVENT";
+ public static final String EXTRA_KEY_EVENT = "EXTRA_KEY_EVENT";
+
+ private final IBinder mBinder = new Binder();
+
+ public TestService() {
+ getLifecycle().addObserver(new LifecycleObserver() {
+ @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
+ public void anyEvent(LifecycleOwner owner, Lifecycle.Event event) {
+ Context context = (TestService) owner;
+ Intent intent = new Intent(ACTION_LOG_EVENT);
+ intent.putExtra(EXTRA_KEY_EVENT, event);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+ });
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ super.onBind(intent);
+ return mBinder;
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/viewmodeltest/TestViewModel.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/viewmodeltest/TestViewModel.java
new file mode 100644
index 0000000..40c27b9
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/viewmodeltest/TestViewModel.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.viewmodeltest;
+
+import android.app.Application;
+
+import androidx.lifecycle.AndroidViewModel;
+
+public class TestViewModel extends AndroidViewModel {
+
+ public TestViewModel(Application application) {
+ super(application);
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/viewmodeltest/ViewModelActivity.java b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/viewmodeltest/ViewModelActivity.java
new file mode 100644
index 0000000..c20e819
--- /dev/null
+++ b/lifecycle/extensions/src/androidTest/java/androidx/lifecycle/viewmodeltest/ViewModelActivity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.viewmodeltest;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.lifecycle.extensions.test.R;
+
+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";
+ public static final String FRAGMENT_TAG_2 = "f2";
+
+ public TestViewModel activityModel;
+ public TestViewModel defaultActivityModel;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_view_model);
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.fragment_container, new ViewModelFragment(), FRAGMENT_TAG_1)
+ .add(new ViewModelFragment(), FRAGMENT_TAG_2)
+ .commit();
+ }
+ activityModel = ViewModelProviders.of(this).get(KEY_ACTIVITY_MODEL, TestViewModel.class);
+ defaultActivityModel = ViewModelProviders.of(this).get(TestViewModel.class);
+ }
+
+ public static class ViewModelFragment extends Fragment {
+ public TestViewModel fragmentModel;
+ public TestViewModel activityModel;
+ public TestViewModel defaultActivityModel;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ fragmentModel = ViewModelProviders.of(this).get(KEY_FRAGMENT_MODEL,
+ TestViewModel.class);
+ activityModel = ViewModelProviders.of(getActivity()).get(KEY_ACTIVITY_MODEL,
+ TestViewModel.class);
+ defaultActivityModel = ViewModelProviders.of(getActivity()).get(TestViewModel.class);
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/androidTest/res/layout/activity_main.xml b/lifecycle/extensions/src/androidTest/res/layout/activity_main.xml
index 305bccc..86730b2 100644
--- a/lifecycle/extensions/src/androidTest/res/layout/activity_main.xml
+++ b/lifecycle/extensions/src/androidTest/res/layout/activity_main.xml
@@ -21,7 +21,7 @@
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context="android.arch.lifecycle.activity.FragmentLifecycleActivity">
+ tools:context="androidx.lifecycle.activity.FragmentLifecycleActivity">
<FrameLayout android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>
diff --git a/lifecycle/extensions/src/main/AndroidManifest.xml b/lifecycle/extensions/src/main/AndroidManifest.xml
index f8a5c48..4706adf 100644
--- a/lifecycle/extensions/src/main/AndroidManifest.xml
+++ b/lifecycle/extensions/src/main/AndroidManifest.xml
@@ -15,11 +15,11 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.extensions">
+ package="androidx.lifecycle.extensions">
<application>
<provider android:authorities="${applicationId}.lifecycle-trojan"
android:multiprocess="true"
android:exported="false"
- android:name="android.arch.lifecycle.ProcessLifecycleOwnerInitializer"/>
+ android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"/>
</application>
</manifest>
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/EmptyActivityLifecycleCallbacks.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/EmptyActivityLifecycleCallbacks.java
deleted file mode 100644
index ed41f2d..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/EmptyActivityLifecycleCallbacks.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.app.Activity;
-import android.app.Application;
-import android.os.Bundle;
-
-class EmptyActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
- @Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
- }
-
- @Override
- public void onActivityStarted(Activity activity) {
- }
-
- @Override
- public void onActivityResumed(Activity activity) {
- }
-
- @Override
- public void onActivityPaused(Activity activity) {
- }
-
- @Override
- public void onActivityStopped(Activity activity) {
- }
-
- @Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
- }
-
- @Override
- public void onActivityDestroyed(Activity activity) {
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/HolderFragment.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/HolderFragment.java
deleted file mode 100644
index ca5e181..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/HolderFragment.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * 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.app.Activity;
-import android.app.Application.ActivityLifecycleCallbacks;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks;
-import android.util.Log;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class HolderFragment extends Fragment implements ViewModelStoreOwner {
- private static final String LOG_TAG = "ViewModelStores";
-
- private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static final String HOLDER_TAG =
- "android.arch.lifecycle.state.StateProviderHolderFragment";
-
- private ViewModelStore mViewModelStore = new ViewModelStore();
-
- public HolderFragment() {
- setRetainInstance(true);
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- sHolderFragmentManager.holderFragmentCreated(this);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mViewModelStore.clear();
- }
-
- @NonNull
- @Override
- public ViewModelStore getViewModelStore() {
- return mViewModelStore;
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static HolderFragment holderFragmentFor(FragmentActivity activity) {
- return sHolderFragmentManager.holderFragmentFor(activity);
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static HolderFragment holderFragmentFor(Fragment fragment) {
- return sHolderFragmentManager.holderFragmentFor(fragment);
- }
-
- @SuppressWarnings("WeakerAccess")
- static class HolderFragmentManager {
- private Map<Activity, HolderFragment> mNotCommittedActivityHolders = new HashMap<>();
- private Map<Fragment, HolderFragment> mNotCommittedFragmentHolders = new HashMap<>();
-
- private ActivityLifecycleCallbacks mActivityCallbacks =
- new EmptyActivityLifecycleCallbacks() {
- @Override
- public void onActivityDestroyed(Activity activity) {
- HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
- if (fragment != null) {
- Log.e(LOG_TAG, "Failed to save a ViewModel for " + activity);
- }
- }
- };
-
- private boolean mActivityCallbacksIsAdded = false;
-
- private FragmentLifecycleCallbacks mParentDestroyedCallback =
- new FragmentLifecycleCallbacks() {
- @Override
- public void onFragmentDestroyed(FragmentManager fm, Fragment parentFragment) {
- super.onFragmentDestroyed(fm, parentFragment);
- HolderFragment fragment = mNotCommittedFragmentHolders.remove(
- parentFragment);
- if (fragment != null) {
- Log.e(LOG_TAG, "Failed to save a ViewModel for " + parentFragment);
- }
- }
- };
-
- void holderFragmentCreated(Fragment holderFragment) {
- Fragment parentFragment = holderFragment.getParentFragment();
- if (parentFragment != null) {
- mNotCommittedFragmentHolders.remove(parentFragment);
- parentFragment.getFragmentManager().unregisterFragmentLifecycleCallbacks(
- mParentDestroyedCallback);
- } else {
- mNotCommittedActivityHolders.remove(holderFragment.getActivity());
- }
- }
-
- private static HolderFragment findHolderFragment(FragmentManager manager) {
- if (manager.isDestroyed()) {
- throw new IllegalStateException("Can't access ViewModels from onDestroy");
- }
-
- Fragment fragmentByTag = manager.findFragmentByTag(HOLDER_TAG);
- if (fragmentByTag != null && !(fragmentByTag instanceof HolderFragment)) {
- throw new IllegalStateException("Unexpected "
- + "fragment instance was returned by HOLDER_TAG");
- }
- return (HolderFragment) fragmentByTag;
- }
-
- private static HolderFragment createHolderFragment(FragmentManager fragmentManager) {
- HolderFragment holder = new HolderFragment();
- fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
- return holder;
- }
-
- HolderFragment holderFragmentFor(FragmentActivity activity) {
- FragmentManager fm = activity.getSupportFragmentManager();
- HolderFragment holder = findHolderFragment(fm);
- if (holder != null) {
- return holder;
- }
- holder = mNotCommittedActivityHolders.get(activity);
- if (holder != null) {
- return holder;
- }
-
- if (!mActivityCallbacksIsAdded) {
- mActivityCallbacksIsAdded = true;
- activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
- }
- holder = createHolderFragment(fm);
- mNotCommittedActivityHolders.put(activity, holder);
- return holder;
- }
-
- HolderFragment holderFragmentFor(Fragment parentFragment) {
- FragmentManager fm = parentFragment.getChildFragmentManager();
- HolderFragment holder = findHolderFragment(fm);
- if (holder != null) {
- return holder;
- }
- holder = mNotCommittedFragmentHolders.get(parentFragment);
- if (holder != null) {
- return holder;
- }
-
- parentFragment.getFragmentManager()
- .registerFragmentLifecycleCallbacks(mParentDestroyedCallback, false);
- holder = createHolderFragment(fm);
- mNotCommittedFragmentHolders.put(parentFragment, holder);
- return holder;
- }
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleDispatcher.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleDispatcher.java
deleted file mode 100644
index 9fdec95..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleDispatcher.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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 android.app.Activity;
-import android.app.Application;
-import android.arch.lifecycle.Lifecycle.State;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-
-import java.util.Collection;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * When initialized, it hooks into the Activity callback of the Application and observes
- * Activities. It is responsible to hook in child-fragments to activities and fragments to report
- * their lifecycle events. Another responsibility of this class is to mark as stopped all lifecycle
- * providers related to an activity as soon it is not safe to run a fragment transaction in this
- * activity.
- */
-class LifecycleDispatcher {
-
- private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle"
- + ".LifecycleDispatcher.report_fragment_tag";
-
- private static AtomicBoolean sInitialized = new AtomicBoolean(false);
-
- static void init(Context context) {
- if (sInitialized.getAndSet(true)) {
- return;
- }
- ((Application) context.getApplicationContext())
- .registerActivityLifecycleCallbacks(new DispatcherActivityCallback());
- }
-
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- static class DispatcherActivityCallback extends EmptyActivityLifecycleCallbacks {
- private final FragmentCallback mFragmentCallback;
-
- DispatcherActivityCallback() {
- mFragmentCallback = new FragmentCallback();
- }
-
- @Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
- if (activity instanceof FragmentActivity) {
- ((FragmentActivity) activity).getSupportFragmentManager()
- .registerFragmentLifecycleCallbacks(mFragmentCallback, true);
- }
- ReportFragment.injectIfNeededIn(activity);
- }
-
- @Override
- public void onActivityStopped(Activity activity) {
- if (activity instanceof FragmentActivity) {
- markState((FragmentActivity) activity, CREATED);
- }
- }
-
- @Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
- if (activity instanceof FragmentActivity) {
- markState((FragmentActivity) activity, CREATED);
- }
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- public static class DestructionReportFragment extends Fragment {
- @Override
- public void onPause() {
- super.onPause();
- dispatch(ON_PAUSE);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- dispatch(ON_STOP);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- dispatch(ON_DESTROY);
- }
-
- protected void dispatch(Lifecycle.Event event) {
- dispatchIfLifecycleOwner(getParentFragment(), event);
- }
- }
-
- private static void markState(FragmentManager manager, State state) {
- Collection<Fragment> fragments = manager.getFragments();
- if (fragments == null) {
- return;
- }
- for (Fragment fragment : fragments) {
- if (fragment == null) {
- continue;
- }
- markStateIn(fragment, state);
- if (fragment.isAdded()) {
- markState(fragment.getChildFragmentManager(), state);
- }
- }
- }
-
- private static void markStateIn(Object object, State state) {
- if (object instanceof LifecycleRegistryOwner) {
- LifecycleRegistry registry = ((LifecycleRegistryOwner) object).getLifecycle();
- registry.markState(state);
- }
- }
-
- private static void markState(FragmentActivity activity, State state) {
- markStateIn(activity, state);
- markState(activity.getSupportFragmentManager(), state);
- }
-
- private static void dispatchIfLifecycleOwner(Fragment fragment, Lifecycle.Event event) {
- if (fragment instanceof LifecycleRegistryOwner) {
- ((LifecycleRegistryOwner) fragment).getLifecycle().handleLifecycleEvent(event);
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- static class FragmentCallback extends FragmentManager.FragmentLifecycleCallbacks {
-
- @Override
- public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {
- dispatchIfLifecycleOwner(f, ON_CREATE);
-
- if (!(f instanceof LifecycleRegistryOwner)) {
- return;
- }
-
- if (f.getChildFragmentManager().findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
- f.getChildFragmentManager().beginTransaction().add(new DestructionReportFragment(),
- REPORT_FRAGMENT_TAG).commit();
- }
- }
-
- @Override
- public void onFragmentStarted(FragmentManager fm, Fragment f) {
- dispatchIfLifecycleOwner(f, ON_START);
- }
-
- @Override
- public void onFragmentResumed(FragmentManager fm, Fragment f) {
- dispatchIfLifecycleOwner(f, ON_RESUME);
- }
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleService.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleService.java
deleted file mode 100644
index adc4ffc..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/LifecycleService.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-import android.support.annotation.CallSuper;
-import android.support.annotation.Nullable;
-
-/**
- * A Service that is also a {@link LifecycleOwner}.
- */
-public class LifecycleService extends Service implements LifecycleOwner {
-
- private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this);
-
- @CallSuper
- @Override
- public void onCreate() {
- mDispatcher.onServicePreSuperOnCreate();
- super.onCreate();
- }
-
- @CallSuper
- @Nullable
- @Override
- public IBinder onBind(Intent intent) {
- mDispatcher.onServicePreSuperOnBind();
- return null;
- }
-
- @SuppressWarnings("deprecation")
- @CallSuper
- @Override
- public void onStart(Intent intent, int startId) {
- mDispatcher.onServicePreSuperOnStart();
- super.onStart(intent, startId);
- }
-
- // this method is added only to annotate it with @CallSuper.
- // In usual service super.onStartCommand is no-op, but in LifecycleService
- // it results in mDispatcher.onServicePreSuperOnStart() call, because
- // super.onStartCommand calls onStart().
- @CallSuper
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- return super.onStartCommand(intent, flags, startId);
- }
-
- @CallSuper
- @Override
- public void onDestroy() {
- mDispatcher.onServicePreSuperOnDestroy();
- super.onDestroy();
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mDispatcher.getLifecycle();
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ProcessLifecycleOwner.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ProcessLifecycleOwner.java
deleted file mode 100644
index 74ea97f..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ProcessLifecycleOwner.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * 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.app.Activity;
-import android.app.Application;
-import android.arch.lifecycle.ReportFragment.ActivityInitializationListener;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-
-/**
- * Class that provides lifecycle for the whole application process.
- * <p>
- * You can consider this LifecycleOwner as the composite of all of your Activities, except that
- * {@link Lifecycle.Event#ON_CREATE} will be dispatched once and {@link Lifecycle.Event#ON_DESTROY}
- * will never be dispatched. Other lifecycle events will be dispatched with following rules:
- * ProcessLifecycleOwner will dispatch {@link Lifecycle.Event#ON_START},
- * {@link Lifecycle.Event#ON_RESUME} events, as a first activity moves through these events.
- * {@link Lifecycle.Event#ON_PAUSE}, {@link Lifecycle.Event#ON_STOP}, events will be dispatched with
- * a <b>delay</b> after a last activity
- * passed through them. This delay is long enough to guarantee that ProcessLifecycleOwner
- * won't send any events if activities are destroyed and recreated due to a
- * configuration change.
- *
- * <p>
- * It is useful for use cases where you would like to react on your app coming to the foreground or
- * going to the background and you don't need a milliseconds accuracy in receiving lifecycle
- * events.
- */
-@SuppressWarnings("WeakerAccess")
-public class ProcessLifecycleOwner implements LifecycleOwner {
-
- @VisibleForTesting
- static final long TIMEOUT_MS = 700; //mls
-
- // ground truth counters
- private int mStartedCounter = 0;
- private int mResumedCounter = 0;
-
- private boolean mPauseSent = true;
- private boolean mStopSent = true;
-
- private Handler mHandler;
- private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
-
- private Runnable mDelayedPauseRunnable = new Runnable() {
- @Override
- public void run() {
- dispatchPauseIfNeeded();
- dispatchStopIfNeeded();
- }
- };
-
- private ActivityInitializationListener mInitializationListener =
- new ActivityInitializationListener() {
- @Override
- public void onCreate() {
- }
-
- @Override
- public void onStart() {
- activityStarted();
- }
-
- @Override
- public void onResume() {
- activityResumed();
- }
- };
-
- private static final ProcessLifecycleOwner sInstance = new ProcessLifecycleOwner();
-
- /**
- * The LifecycleOwner for the whole application process. Note that if your application
- * has multiple processes, this provider does not know about other processes.
- *
- * @return {@link LifecycleOwner} for the whole application.
- */
- public static LifecycleOwner get() {
- return sInstance;
- }
-
- static void init(Context context) {
- sInstance.attach(context);
- }
-
- void activityStarted() {
- mStartedCounter++;
- if (mStartedCounter == 1 && mStopSent) {
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- mStopSent = false;
- }
- }
-
- void activityResumed() {
- mResumedCounter++;
- if (mResumedCounter == 1) {
- if (mPauseSent) {
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
- mPauseSent = false;
- } else {
- mHandler.removeCallbacks(mDelayedPauseRunnable);
- }
- }
- }
-
- void activityPaused() {
- mResumedCounter--;
- if (mResumedCounter == 0) {
- mHandler.postDelayed(mDelayedPauseRunnable, TIMEOUT_MS);
- }
- }
-
- void activityStopped() {
- mStartedCounter--;
- dispatchStopIfNeeded();
- }
-
- private void dispatchPauseIfNeeded() {
- if (mResumedCounter == 0) {
- mPauseSent = true;
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
- }
- }
-
- private void dispatchStopIfNeeded() {
- if (mStartedCounter == 0 && mPauseSent) {
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- mStopSent = true;
- }
- }
-
- private ProcessLifecycleOwner() {
- }
-
- void attach(Context context) {
- mHandler = new Handler();
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
- Application app = (Application) context.getApplicationContext();
- app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
- @Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
- ReportFragment.get(activity).setProcessListener(mInitializationListener);
- }
-
- @Override
- public void onActivityPaused(Activity activity) {
- activityPaused();
- }
-
- @Override
- public void onActivityStopped(Activity activity) {
- activityStopped();
- }
- });
- }
-
- @NonNull
- @Override
- public Lifecycle getLifecycle() {
- return mRegistry;
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java
deleted file mode 100644
index 6cf80d2..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-/**
- * Internal class to initialize Lifecycles.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ProcessLifecycleOwnerInitializer extends ContentProvider {
- @Override
- public boolean onCreate() {
- LifecycleDispatcher.init(getContext());
- ProcessLifecycleOwner.init(getContext());
- return true;
- }
-
- @Nullable
- @Override
- public Cursor query(@NonNull Uri uri, String[] strings, String s, String[] strings1,
- String s1) {
- return null;
- }
-
- @Nullable
- @Override
- public String getType(@NonNull Uri uri) {
- return null;
- }
-
- @Nullable
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
- return null;
- }
-
- @Override
- public int delete(@NonNull Uri uri, String s, String[] strings) {
- return 0;
- }
-
- @Override
- public int update(@NonNull Uri uri, ContentValues contentValues, String s, String[] strings) {
- return 0;
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ServiceLifecycleDispatcher.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ServiceLifecycleDispatcher.java
deleted file mode 100644
index 6067d7b..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ServiceLifecycleDispatcher.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.app.Service;
-import android.content.Intent;
-import android.os.Handler;
-import android.support.annotation.NonNull;
-
-/**
- * Helper class to dispatch lifecycle events for a service. Use it only if it is impossible
- * to use {@link LifecycleService}.
- */
-@SuppressWarnings("WeakerAccess")
-public class ServiceLifecycleDispatcher {
- private final LifecycleRegistry mRegistry;
- private final Handler mHandler;
- private DispatchRunnable mLastDispatchRunnable;
-
- /**
- * @param provider {@link LifecycleOwner} for a service, usually it is a service itself
- */
- public ServiceLifecycleDispatcher(@NonNull LifecycleOwner provider) {
- mRegistry = new LifecycleRegistry(provider);
- mHandler = new Handler();
- }
-
- private void postDispatchRunnable(Lifecycle.Event event) {
- if (mLastDispatchRunnable != null) {
- mLastDispatchRunnable.run();
- }
- mLastDispatchRunnable = new DispatchRunnable(mRegistry, event);
- mHandler.postAtFrontOfQueue(mLastDispatchRunnable);
- }
-
- /**
- * Must be a first call in {@link Service#onCreate()} method, even before super.onCreate call.
- */
- public void onServicePreSuperOnCreate() {
- postDispatchRunnable(Lifecycle.Event.ON_CREATE);
- }
-
- /**
- * Must be a first call in {@link Service#onBind(Intent)} method, even before super.onBind
- * call.
- */
- public void onServicePreSuperOnBind() {
- postDispatchRunnable(Lifecycle.Event.ON_START);
- }
-
- /**
- * Must be a first call in {@link Service#onStart(Intent, int)} or
- * {@link Service#onStartCommand(Intent, int, int)} methods, even before
- * a corresponding super call.
- */
- public void onServicePreSuperOnStart() {
- postDispatchRunnable(Lifecycle.Event.ON_START);
- }
-
- /**
- * Must be a first call in {@link Service#onDestroy()} method, even before super.OnDestroy
- * call.
- */
- public void onServicePreSuperOnDestroy() {
- postDispatchRunnable(Lifecycle.Event.ON_STOP);
- postDispatchRunnable(Lifecycle.Event.ON_DESTROY);
- }
-
- /**
- * @return {@link Lifecycle} for the given {@link LifecycleOwner}
- */
- public Lifecycle getLifecycle() {
- return mRegistry;
- }
-
- static class DispatchRunnable implements Runnable {
- private final LifecycleRegistry mRegistry;
- final Lifecycle.Event mEvent;
- private boolean mWasExecuted = false;
-
- DispatchRunnable(@NonNull LifecycleRegistry registry, Lifecycle.Event event) {
- mRegistry = registry;
- mEvent = event;
- }
-
- @Override
- public void run() {
- if (!mWasExecuted) {
- mRegistry.handleLifecycleEvent(mEvent);
- mWasExecuted = true;
- }
- }
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java
deleted file mode 100644
index 0940235..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelProviders.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.app.Activity;
-import android.app.Application;
-import android.arch.lifecycle.ViewModelProvider.Factory;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-
-/**
- * Utilities methods for {@link ViewModelStore} class.
- */
-public class ViewModelProviders {
-
- /**
- * @deprecated This class should not be directly instantiated
- */
- @Deprecated
- public ViewModelProviders() {
- }
-
- 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}.
- * <p>
- * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
- *
- * @param fragment a fragment, in whose scope ViewModels should be retained
- * @return a ViewModelProvider instance
- */
- @NonNull
- @MainThread
- public static ViewModelProvider of(@NonNull Fragment fragment) {
- return of(fragment, null);
- }
-
- /**
- * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
- * is alive. More detailed explanation is in {@link ViewModel}.
- * <p>
- * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
- *
- * @param activity an activity, in whose scope ViewModels should be retained
- * @return a ViewModelProvider instance
- */
- @NonNull
- @MainThread
- public static ViewModelProvider of(@NonNull FragmentActivity activity) {
- return of(activity, null);
- }
-
- /**
- * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
- * {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
- * <p>
- * It uses the given {@link Factory} to instantiate new ViewModels.
- *
- * @param fragment a fragment, in whose scope ViewModels should be retained
- * @param factory a {@code Factory} to instantiate new ViewModels
- * @return a ViewModelProvider instance
- */
- @NonNull
- @MainThread
- public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
- Application application = checkApplication(checkActivity(fragment));
- if (factory == null) {
- factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
- }
- return new ViewModelProvider(ViewModelStores.of(fragment), factory);
- }
-
- /**
- * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
- * is alive. More detailed explanation is in {@link ViewModel}.
- * <p>
- * It uses the given {@link Factory} to instantiate new ViewModels.
- *
- * @param activity an activity, in whose scope ViewModels should be retained
- * @param factory a {@code Factory} to instantiate new ViewModels
- * @return a ViewModelProvider instance
- */
- @NonNull
- @MainThread
- public static ViewModelProvider of(@NonNull FragmentActivity activity,
- @Nullable Factory factory) {
- Application application = checkApplication(activity);
- if (factory == null) {
- factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
- }
- return new ViewModelProvider(ViewModelStores.of(activity), factory);
- }
-
- /**
- * {@link Factory} which may create {@link AndroidViewModel} and
- * {@link ViewModel}, which have an empty constructor.
- *
- * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory}
- */
- @SuppressWarnings("WeakerAccess")
- @Deprecated
- public static class DefaultFactory extends ViewModelProvider.AndroidViewModelFactory {
- /**
- * Creates a {@code AndroidViewModelFactory}
- *
- * @param application an application to pass in {@link AndroidViewModel}
- * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory} or
- * {@link ViewModelProvider.AndroidViewModelFactory#getInstance(Application)}.
- */
- @Deprecated
- public DefaultFactory(@NonNull Application application) {
- super(application);
- }
- }
-}
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java
deleted file mode 100644
index 348a06e..0000000
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/ViewModelStores.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.HolderFragment.holderFragmentFor;
-
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-
-/**
- * Factory methods for {@link ViewModelStore} class.
- */
-@SuppressWarnings("WeakerAccess")
-public class ViewModelStores {
-
- private ViewModelStores() {
- }
-
- /**
- * Returns the {@link ViewModelStore} of the given activity.
- *
- * @param activity an activity whose {@code ViewModelStore} is requested
- * @return a {@code ViewModelStore}
- */
- @NonNull
- @MainThread
- public static ViewModelStore of(@NonNull FragmentActivity activity) {
- if (activity instanceof ViewModelStoreOwner) {
- return ((ViewModelStoreOwner) activity).getViewModelStore();
- }
- return holderFragmentFor(activity).getViewModelStore();
- }
-
- /**
- * Returns the {@link ViewModelStore} of the given fragment.
- *
- * @param fragment a fragment whose {@code ViewModelStore} is requested
- * @return a {@code ViewModelStore}
- */
- @NonNull
- @MainThread
- public static ViewModelStore of(@NonNull Fragment fragment) {
- if (fragment instanceof ViewModelStoreOwner) {
- return ((ViewModelStoreOwner) fragment).getViewModelStore();
- }
- return holderFragmentFor(fragment).getViewModelStore();
- }
-}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/EmptyActivityLifecycleCallbacks.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/EmptyActivityLifecycleCallbacks.java
new file mode 100644
index 0000000..70518a6
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/EmptyActivityLifecycleCallbacks.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 androidx.lifecycle;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+class EmptyActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ }
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/HolderFragment.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/HolderFragment.java
new file mode 100644
index 0000000..24fa504
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/HolderFragment.java
@@ -0,0 +1,195 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Activity;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class HolderFragment extends Fragment implements ViewModelStoreOwner {
+ private static final String LOG_TAG = "ViewModelStores";
+
+ private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final String HOLDER_TAG =
+ "androidx.lifecycle.state.StateProviderHolderFragment";
+
+ private ViewModelStore mViewModelStore = new ViewModelStore();
+
+ public HolderFragment() {
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sHolderFragmentManager.holderFragmentCreated(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mViewModelStore.clear();
+ }
+
+ @NonNull
+ @Override
+ public ViewModelStore getViewModelStore() {
+ return mViewModelStore;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static HolderFragment holderFragmentFor(FragmentActivity activity) {
+ return sHolderFragmentManager.holderFragmentFor(activity);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static HolderFragment holderFragmentFor(Fragment fragment) {
+ return sHolderFragmentManager.holderFragmentFor(fragment);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class HolderFragmentManager {
+ private Map<Activity, HolderFragment> mNotCommittedActivityHolders = new HashMap<>();
+ private Map<Fragment, HolderFragment> mNotCommittedFragmentHolders = new HashMap<>();
+
+ private ActivityLifecycleCallbacks mActivityCallbacks =
+ new EmptyActivityLifecycleCallbacks() {
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
+ if (fragment != null) {
+ Log.e(LOG_TAG, "Failed to save a ViewModel for " + activity);
+ }
+ }
+ };
+
+ private boolean mActivityCallbacksIsAdded = false;
+
+ private FragmentLifecycleCallbacks mParentDestroyedCallback =
+ new FragmentLifecycleCallbacks() {
+ @Override
+ public void onFragmentDestroyed(FragmentManager fm, Fragment parentFragment) {
+ super.onFragmentDestroyed(fm, parentFragment);
+ HolderFragment fragment = mNotCommittedFragmentHolders.remove(
+ parentFragment);
+ if (fragment != null) {
+ Log.e(LOG_TAG, "Failed to save a ViewModel for " + parentFragment);
+ }
+ }
+ };
+
+ void holderFragmentCreated(Fragment holderFragment) {
+ Fragment parentFragment = holderFragment.getParentFragment();
+ if (parentFragment != null) {
+ mNotCommittedFragmentHolders.remove(parentFragment);
+ parentFragment.getFragmentManager().unregisterFragmentLifecycleCallbacks(
+ mParentDestroyedCallback);
+ } else {
+ mNotCommittedActivityHolders.remove(holderFragment.getActivity());
+ }
+ }
+
+ private static HolderFragment findHolderFragment(FragmentManager manager) {
+ if (manager.isDestroyed()) {
+ throw new IllegalStateException("Can't access ViewModels from onDestroy");
+ }
+
+ Fragment fragmentByTag = manager.findFragmentByTag(HOLDER_TAG);
+ if (fragmentByTag != null && !(fragmentByTag instanceof HolderFragment)) {
+ throw new IllegalStateException("Unexpected "
+ + "fragment instance was returned by HOLDER_TAG");
+ }
+ return (HolderFragment) fragmentByTag;
+ }
+
+ private static HolderFragment createHolderFragment(FragmentManager fragmentManager) {
+ HolderFragment holder = new HolderFragment();
+ fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
+ return holder;
+ }
+
+ HolderFragment holderFragmentFor(FragmentActivity activity) {
+ FragmentManager fm = activity.getSupportFragmentManager();
+ HolderFragment holder = findHolderFragment(fm);
+ if (holder != null) {
+ return holder;
+ }
+ holder = mNotCommittedActivityHolders.get(activity);
+ if (holder != null) {
+ return holder;
+ }
+
+ if (!mActivityCallbacksIsAdded) {
+ mActivityCallbacksIsAdded = true;
+ activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
+ }
+ holder = createHolderFragment(fm);
+ mNotCommittedActivityHolders.put(activity, holder);
+ return holder;
+ }
+
+ HolderFragment holderFragmentFor(Fragment parentFragment) {
+ FragmentManager fm = parentFragment.getChildFragmentManager();
+ HolderFragment holder = findHolderFragment(fm);
+ if (holder != null) {
+ return holder;
+ }
+ holder = mNotCommittedFragmentHolders.get(parentFragment);
+ if (holder != null) {
+ return holder;
+ }
+
+ parentFragment.getFragmentManager()
+ .registerFragmentLifecycleCallbacks(mParentDestroyedCallback, false);
+ holder = createHolderFragment(fm);
+ mNotCommittedFragmentHolders.put(parentFragment, holder);
+ return holder;
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/LifecycleDispatcher.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/LifecycleDispatcher.java
new file mode 100644
index 0000000..f241276
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/LifecycleDispatcher.java
@@ -0,0 +1,183 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle.State;
+
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * When initialized, it hooks into the Activity callback of the Application and observes
+ * Activities. It is responsible to hook in child-fragments to activities and fragments to report
+ * their lifecycle events. Another responsibility of this class is to mark as stopped all lifecycle
+ * providers related to an activity as soon it is not safe to run a fragment transaction in this
+ * activity.
+ */
+class LifecycleDispatcher {
+
+ private static final String REPORT_FRAGMENT_TAG = "androidx.lifecycle"
+ + ".LifecycleDispatcher.report_fragment_tag";
+
+ private static AtomicBoolean sInitialized = new AtomicBoolean(false);
+
+ static void init(Context context) {
+ if (sInitialized.getAndSet(true)) {
+ return;
+ }
+ ((Application) context.getApplicationContext())
+ .registerActivityLifecycleCallbacks(new DispatcherActivityCallback());
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ static class DispatcherActivityCallback extends EmptyActivityLifecycleCallbacks {
+ private final FragmentCallback mFragmentCallback;
+
+ DispatcherActivityCallback() {
+ mFragmentCallback = new FragmentCallback();
+ }
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ if (activity instanceof FragmentActivity) {
+ ((FragmentActivity) activity).getSupportFragmentManager()
+ .registerFragmentLifecycleCallbacks(mFragmentCallback, true);
+ }
+ ReportFragment.injectIfNeededIn(activity);
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ if (activity instanceof FragmentActivity) {
+ markState((FragmentActivity) activity, CREATED);
+ }
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ if (activity instanceof FragmentActivity) {
+ markState((FragmentActivity) activity, CREATED);
+ }
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ public static class DestructionReportFragment extends Fragment {
+ @Override
+ public void onPause() {
+ super.onPause();
+ dispatch(ON_PAUSE);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ dispatch(ON_STOP);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ dispatch(ON_DESTROY);
+ }
+
+ protected void dispatch(Lifecycle.Event event) {
+ dispatchIfLifecycleOwner(getParentFragment(), event);
+ }
+ }
+
+ private static void markState(FragmentManager manager, State state) {
+ Collection<Fragment> fragments = manager.getFragments();
+ if (fragments == null) {
+ return;
+ }
+ for (Fragment fragment : fragments) {
+ if (fragment == null) {
+ continue;
+ }
+ markStateIn(fragment, state);
+ if (fragment.isAdded()) {
+ markState(fragment.getChildFragmentManager(), state);
+ }
+ }
+ }
+
+ private static void markStateIn(Object object, State state) {
+ if (object instanceof LifecycleRegistryOwner) {
+ LifecycleRegistry registry = ((LifecycleRegistryOwner) object).getLifecycle();
+ registry.markState(state);
+ }
+ }
+
+ private static void markState(FragmentActivity activity, State state) {
+ markStateIn(activity, state);
+ markState(activity.getSupportFragmentManager(), state);
+ }
+
+ private static void dispatchIfLifecycleOwner(Fragment fragment, Lifecycle.Event event) {
+ if (fragment instanceof LifecycleRegistryOwner) {
+ ((LifecycleRegistryOwner) fragment).getLifecycle().handleLifecycleEvent(event);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ static class FragmentCallback extends FragmentManager.FragmentLifecycleCallbacks {
+
+ @Override
+ public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {
+ dispatchIfLifecycleOwner(f, ON_CREATE);
+
+ if (!(f instanceof LifecycleRegistryOwner)) {
+ return;
+ }
+
+ if (f.getChildFragmentManager().findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
+ f.getChildFragmentManager().beginTransaction().add(new DestructionReportFragment(),
+ REPORT_FRAGMENT_TAG).commit();
+ }
+ }
+
+ @Override
+ public void onFragmentStarted(FragmentManager fm, Fragment f) {
+ dispatchIfLifecycleOwner(f, ON_START);
+ }
+
+ @Override
+ public void onFragmentResumed(FragmentManager fm, Fragment f) {
+ dispatchIfLifecycleOwner(f, ON_RESUME);
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/LifecycleService.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/LifecycleService.java
new file mode 100644
index 0000000..8275573
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/LifecycleService.java
@@ -0,0 +1,77 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+
+/**
+ * A Service that is also a {@link LifecycleOwner}.
+ */
+public class LifecycleService extends Service implements LifecycleOwner {
+
+ private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this);
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ mDispatcher.onServicePreSuperOnCreate();
+ super.onCreate();
+ }
+
+ @CallSuper
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ mDispatcher.onServicePreSuperOnBind();
+ return null;
+ }
+
+ @SuppressWarnings("deprecation")
+ @CallSuper
+ @Override
+ public void onStart(Intent intent, int startId) {
+ mDispatcher.onServicePreSuperOnStart();
+ super.onStart(intent, startId);
+ }
+
+ // this method is added only to annotate it with @CallSuper.
+ // In usual service super.onStartCommand is no-op, but in LifecycleService
+ // it results in mDispatcher.onServicePreSuperOnStart() call, because
+ // super.onStartCommand calls onStart().
+ @CallSuper
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @CallSuper
+ @Override
+ public void onDestroy() {
+ mDispatcher.onServicePreSuperOnDestroy();
+ super.onDestroy();
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mDispatcher.getLifecycle();
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.java
new file mode 100644
index 0000000..73513eb
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.java
@@ -0,0 +1,181 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.ReportFragment.ActivityInitializationListener;
+
+/**
+ * Class that provides lifecycle for the whole application process.
+ * <p>
+ * You can consider this LifecycleOwner as the composite of all of your Activities, except that
+ * {@link Lifecycle.Event#ON_CREATE} will be dispatched once and {@link Lifecycle.Event#ON_DESTROY}
+ * will never be dispatched. Other lifecycle events will be dispatched with following rules:
+ * ProcessLifecycleOwner will dispatch {@link Lifecycle.Event#ON_START},
+ * {@link Lifecycle.Event#ON_RESUME} events, as a first activity moves through these events.
+ * {@link Lifecycle.Event#ON_PAUSE}, {@link Lifecycle.Event#ON_STOP}, events will be dispatched with
+ * a <b>delay</b> after a last activity
+ * passed through them. This delay is long enough to guarantee that ProcessLifecycleOwner
+ * won't send any events if activities are destroyed and recreated due to a
+ * configuration change.
+ *
+ * <p>
+ * It is useful for use cases where you would like to react on your app coming to the foreground or
+ * going to the background and you don't need a milliseconds accuracy in receiving lifecycle
+ * events.
+ */
+@SuppressWarnings("WeakerAccess")
+public class ProcessLifecycleOwner implements LifecycleOwner {
+
+ @VisibleForTesting
+ static final long TIMEOUT_MS = 700; //mls
+
+ // ground truth counters
+ private int mStartedCounter = 0;
+ private int mResumedCounter = 0;
+
+ private boolean mPauseSent = true;
+ private boolean mStopSent = true;
+
+ private Handler mHandler;
+ private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
+
+ private Runnable mDelayedPauseRunnable = new Runnable() {
+ @Override
+ public void run() {
+ dispatchPauseIfNeeded();
+ dispatchStopIfNeeded();
+ }
+ };
+
+ private ActivityInitializationListener mInitializationListener =
+ new ActivityInitializationListener() {
+ @Override
+ public void onCreate() {
+ }
+
+ @Override
+ public void onStart() {
+ activityStarted();
+ }
+
+ @Override
+ public void onResume() {
+ activityResumed();
+ }
+ };
+
+ private static final ProcessLifecycleOwner sInstance = new ProcessLifecycleOwner();
+
+ /**
+ * The LifecycleOwner for the whole application process. Note that if your application
+ * has multiple processes, this provider does not know about other processes.
+ *
+ * @return {@link LifecycleOwner} for the whole application.
+ */
+ public static LifecycleOwner get() {
+ return sInstance;
+ }
+
+ static void init(Context context) {
+ sInstance.attach(context);
+ }
+
+ void activityStarted() {
+ mStartedCounter++;
+ if (mStartedCounter == 1 && mStopSent) {
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mStopSent = false;
+ }
+ }
+
+ void activityResumed() {
+ mResumedCounter++;
+ if (mResumedCounter == 1) {
+ if (mPauseSent) {
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+ mPauseSent = false;
+ } else {
+ mHandler.removeCallbacks(mDelayedPauseRunnable);
+ }
+ }
+ }
+
+ void activityPaused() {
+ mResumedCounter--;
+ if (mResumedCounter == 0) {
+ mHandler.postDelayed(mDelayedPauseRunnable, TIMEOUT_MS);
+ }
+ }
+
+ void activityStopped() {
+ mStartedCounter--;
+ dispatchStopIfNeeded();
+ }
+
+ private void dispatchPauseIfNeeded() {
+ if (mResumedCounter == 0) {
+ mPauseSent = true;
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
+ }
+ }
+
+ private void dispatchStopIfNeeded() {
+ if (mStartedCounter == 0 && mPauseSent) {
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ mStopSent = true;
+ }
+ }
+
+ private ProcessLifecycleOwner() {
+ }
+
+ void attach(Context context) {
+ mHandler = new Handler();
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ Application app = (Application) context.getApplicationContext();
+ app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ ReportFragment.get(activity).setProcessListener(mInitializationListener);
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+ activityPaused();
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ activityStopped();
+ }
+ });
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return mRegistry;
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/ProcessLifecycleOwnerInitializer.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/ProcessLifecycleOwnerInitializer.java
new file mode 100644
index 0000000..e20eca0
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/ProcessLifecycleOwnerInitializer.java
@@ -0,0 +1,69 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Internal class to initialize Lifecycles.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ProcessLifecycleOwnerInitializer extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ LifecycleDispatcher.init(getContext());
+ ProcessLifecycleOwner.init(getContext());
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, String[] strings, String s, String[] strings1,
+ String s1) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
+ return null;
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, String s, String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, ContentValues contentValues, String s, String[] strings) {
+ return 0;
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.java
new file mode 100644
index 0000000..23d296e
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.java
@@ -0,0 +1,109 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Helper class to dispatch lifecycle events for a service. Use it only if it is impossible
+ * to use {@link LifecycleService}.
+ */
+@SuppressWarnings("WeakerAccess")
+public class ServiceLifecycleDispatcher {
+ private final LifecycleRegistry mRegistry;
+ private final Handler mHandler;
+ private DispatchRunnable mLastDispatchRunnable;
+
+ /**
+ * @param provider {@link LifecycleOwner} for a service, usually it is a service itself
+ */
+ public ServiceLifecycleDispatcher(@NonNull LifecycleOwner provider) {
+ mRegistry = new LifecycleRegistry(provider);
+ mHandler = new Handler();
+ }
+
+ private void postDispatchRunnable(Lifecycle.Event event) {
+ if (mLastDispatchRunnable != null) {
+ mLastDispatchRunnable.run();
+ }
+ mLastDispatchRunnable = new DispatchRunnable(mRegistry, event);
+ mHandler.postAtFrontOfQueue(mLastDispatchRunnable);
+ }
+
+ /**
+ * Must be a first call in {@link Service#onCreate()} method, even before super.onCreate call.
+ */
+ public void onServicePreSuperOnCreate() {
+ postDispatchRunnable(Lifecycle.Event.ON_CREATE);
+ }
+
+ /**
+ * Must be a first call in {@link Service#onBind(Intent)} method, even before super.onBind
+ * call.
+ */
+ public void onServicePreSuperOnBind() {
+ postDispatchRunnable(Lifecycle.Event.ON_START);
+ }
+
+ /**
+ * Must be a first call in {@link Service#onStart(Intent, int)} or
+ * {@link Service#onStartCommand(Intent, int, int)} methods, even before
+ * a corresponding super call.
+ */
+ public void onServicePreSuperOnStart() {
+ postDispatchRunnable(Lifecycle.Event.ON_START);
+ }
+
+ /**
+ * Must be a first call in {@link Service#onDestroy()} method, even before super.OnDestroy
+ * call.
+ */
+ public void onServicePreSuperOnDestroy() {
+ postDispatchRunnable(Lifecycle.Event.ON_STOP);
+ postDispatchRunnable(Lifecycle.Event.ON_DESTROY);
+ }
+
+ /**
+ * @return {@link Lifecycle} for the given {@link LifecycleOwner}
+ */
+ public Lifecycle getLifecycle() {
+ return mRegistry;
+ }
+
+ static class DispatchRunnable implements Runnable {
+ private final LifecycleRegistry mRegistry;
+ final Lifecycle.Event mEvent;
+ private boolean mWasExecuted = false;
+
+ DispatchRunnable(@NonNull LifecycleRegistry registry, Lifecycle.Event event) {
+ mRegistry = registry;
+ mEvent = event;
+ }
+
+ @Override
+ public void run() {
+ if (!mWasExecuted) {
+ mRegistry.handleLifecycleEvent(mEvent);
+ mWasExecuted = true;
+ }
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/ViewModelProviders.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/ViewModelProviders.java
new file mode 100644
index 0000000..ad72a54
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/ViewModelProviders.java
@@ -0,0 +1,150 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Activity;
+import android.app.Application;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider.Factory;
+
+/**
+ * Utilities methods for {@link ViewModelStore} class.
+ */
+public class ViewModelProviders {
+
+ /**
+ * @deprecated This class should not be directly instantiated
+ */
+ @Deprecated
+ public ViewModelProviders() {
+ }
+
+ 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}.
+ * <p>
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
+ *
+ * @param fragment a fragment, in whose scope ViewModels should be retained
+ * @return a ViewModelProvider instance
+ */
+ @NonNull
+ @MainThread
+ public static ViewModelProvider of(@NonNull Fragment fragment) {
+ return of(fragment, null);
+ }
+
+ /**
+ * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
+ * is alive. More detailed explanation is in {@link ViewModel}.
+ * <p>
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
+ *
+ * @param activity an activity, in whose scope ViewModels should be retained
+ * @return a ViewModelProvider instance
+ */
+ @NonNull
+ @MainThread
+ public static ViewModelProvider of(@NonNull FragmentActivity activity) {
+ return of(activity, null);
+ }
+
+ /**
+ * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
+ * {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
+ * <p>
+ * It uses the given {@link Factory} to instantiate new ViewModels.
+ *
+ * @param fragment a fragment, in whose scope ViewModels should be retained
+ * @param factory a {@code Factory} to instantiate new ViewModels
+ * @return a ViewModelProvider instance
+ */
+ @NonNull
+ @MainThread
+ public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
+ Application application = checkApplication(checkActivity(fragment));
+ if (factory == null) {
+ factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
+ }
+ return new ViewModelProvider(ViewModelStores.of(fragment), factory);
+ }
+
+ /**
+ * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
+ * is alive. More detailed explanation is in {@link ViewModel}.
+ * <p>
+ * It uses the given {@link Factory} to instantiate new ViewModels.
+ *
+ * @param activity an activity, in whose scope ViewModels should be retained
+ * @param factory a {@code Factory} to instantiate new ViewModels
+ * @return a ViewModelProvider instance
+ */
+ @NonNull
+ @MainThread
+ public static ViewModelProvider of(@NonNull FragmentActivity activity,
+ @Nullable Factory factory) {
+ Application application = checkApplication(activity);
+ if (factory == null) {
+ factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
+ }
+ return new ViewModelProvider(ViewModelStores.of(activity), factory);
+ }
+
+ /**
+ * {@link Factory} which may create {@link AndroidViewModel} and
+ * {@link ViewModel}, which have an empty constructor.
+ *
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @Deprecated
+ public static class DefaultFactory extends ViewModelProvider.AndroidViewModelFactory {
+ /**
+ * Creates a {@code AndroidViewModelFactory}
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory} or
+ * {@link ViewModelProvider.AndroidViewModelFactory#getInstance(Application)}.
+ */
+ @Deprecated
+ public DefaultFactory(@NonNull Application application) {
+ super(application);
+ }
+ }
+}
diff --git a/lifecycle/extensions/src/main/java/androidx/lifecycle/ViewModelStores.java b/lifecycle/extensions/src/main/java/androidx/lifecycle/ViewModelStores.java
new file mode 100644
index 0000000..6627239
--- /dev/null
+++ b/lifecycle/extensions/src/main/java/androidx/lifecycle/ViewModelStores.java
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.HolderFragment.holderFragmentFor;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Factory methods for {@link ViewModelStore} class.
+ */
+@SuppressWarnings("WeakerAccess")
+public class ViewModelStores {
+
+ private ViewModelStores() {
+ }
+
+ /**
+ * Returns the {@link ViewModelStore} of the given activity.
+ *
+ * @param activity an activity whose {@code ViewModelStore} is requested
+ * @return a {@code ViewModelStore}
+ */
+ @NonNull
+ @MainThread
+ public static ViewModelStore of(@NonNull FragmentActivity activity) {
+ if (activity instanceof ViewModelStoreOwner) {
+ return ((ViewModelStoreOwner) activity).getViewModelStore();
+ }
+ return holderFragmentFor(activity).getViewModelStore();
+ }
+
+ /**
+ * Returns the {@link ViewModelStore} of the given fragment.
+ *
+ * @param fragment a fragment whose {@code ViewModelStore} is requested
+ * @return a {@code ViewModelStore}
+ */
+ @NonNull
+ @MainThread
+ public static ViewModelStore of(@NonNull Fragment fragment) {
+ if (fragment instanceof ViewModelStoreOwner) {
+ return ((ViewModelStoreOwner) fragment).getViewModelStore();
+ }
+ return holderFragmentFor(fragment).getViewModelStore();
+ }
+}
diff --git a/lifecycle/extensions/src/test/java/android/arch/lifecycle/DispatcherActivityCallbackTest.java b/lifecycle/extensions/src/test/java/android/arch/lifecycle/DispatcherActivityCallbackTest.java
deleted file mode 100644
index 86b25b6..0000000
--- a/lifecycle/extensions/src/test/java/android/arch/lifecycle/DispatcherActivityCallbackTest.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentTransaction;
-import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class DispatcherActivityCallbackTest {
- @Test
- public void onCreateFrameworkActivity() {
- LifecycleDispatcher.DispatcherActivityCallback callback =
- new LifecycleDispatcher.DispatcherActivityCallback();
- Activity activity = mock(Activity.class);
- checkReportFragment(callback, activity);
- }
-
- @Test
- public void onCreateFragmentActivity() {
- LifecycleDispatcher.DispatcherActivityCallback callback =
- new LifecycleDispatcher.DispatcherActivityCallback();
- FragmentActivity activity = mock(FragmentActivity.class);
- FragmentManager fragmentManager = mock(FragmentManager.class);
- when(activity.getSupportFragmentManager()).thenReturn(fragmentManager);
-
- checkReportFragment(callback, activity);
-
- verify(activity).getSupportFragmentManager();
- verify(fragmentManager).registerFragmentLifecycleCallbacks(
- any(FragmentManager.FragmentLifecycleCallbacks.class), eq(true));
- }
-
- @SuppressLint("CommitTransaction")
- private void checkReportFragment(LifecycleDispatcher.DispatcherActivityCallback callback,
- Activity activity) {
- android.app.FragmentManager fm = mock(android.app.FragmentManager.class);
- FragmentTransaction transaction = mock(FragmentTransaction.class);
- when(activity.getFragmentManager()).thenReturn(fm);
- when(fm.beginTransaction()).thenReturn(transaction);
- when(transaction.add(any(Fragment.class), anyString())).thenReturn(transaction);
- callback.onActivityCreated(activity, mock(Bundle.class));
- verify(activity).getFragmentManager();
- verify(fm).beginTransaction();
- verify(transaction).add(any(ReportFragment.class), anyString());
- verify(transaction).commit();
- }
-}
diff --git a/lifecycle/extensions/src/test/java/android/arch/lifecycle/ViewModelProvidersTest.java b/lifecycle/extensions/src/test/java/android/arch/lifecycle/ViewModelProvidersTest.java
deleted file mode 100644
index f37c9a2..0000000
--- a/lifecycle/extensions/src/test/java/android/arch/lifecycle/ViewModelProvidersTest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 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.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class ViewModelProvidersTest {
-
- @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());
- }
-}
diff --git a/lifecycle/extensions/src/test/java/androidx/lifecycle/DispatcherActivityCallbackTest.java b/lifecycle/extensions/src/test/java/androidx/lifecycle/DispatcherActivityCallbackTest.java
new file mode 100644
index 0000000..dba2488
--- /dev/null
+++ b/lifecycle/extensions/src/test/java/androidx/lifecycle/DispatcherActivityCallbackTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DispatcherActivityCallbackTest {
+ @Test
+ public void onCreateFrameworkActivity() {
+ LifecycleDispatcher.DispatcherActivityCallback callback =
+ new LifecycleDispatcher.DispatcherActivityCallback();
+ Activity activity = mock(Activity.class);
+ checkReportFragment(callback, activity);
+ }
+
+ @Test
+ public void onCreateFragmentActivity() {
+ LifecycleDispatcher.DispatcherActivityCallback callback =
+ new LifecycleDispatcher.DispatcherActivityCallback();
+ FragmentActivity activity = mock(FragmentActivity.class);
+ FragmentManager fragmentManager = mock(FragmentManager.class);
+ when(activity.getSupportFragmentManager()).thenReturn(fragmentManager);
+
+ checkReportFragment(callback, activity);
+
+ verify(activity).getSupportFragmentManager();
+ verify(fragmentManager).registerFragmentLifecycleCallbacks(
+ any(FragmentManager.FragmentLifecycleCallbacks.class), eq(true));
+ }
+
+ @SuppressLint("CommitTransaction")
+ private void checkReportFragment(LifecycleDispatcher.DispatcherActivityCallback callback,
+ Activity activity) {
+ android.app.FragmentManager fm = mock(android.app.FragmentManager.class);
+ FragmentTransaction transaction = mock(FragmentTransaction.class);
+ when(activity.getFragmentManager()).thenReturn(fm);
+ when(fm.beginTransaction()).thenReturn(transaction);
+ when(transaction.add(any(Fragment.class), anyString())).thenReturn(transaction);
+ callback.onActivityCreated(activity, mock(Bundle.class));
+ verify(activity).getFragmentManager();
+ verify(fm).beginTransaction();
+ verify(transaction).add(any(ReportFragment.class), anyString());
+ verify(transaction).commit();
+ }
+}
diff --git a/lifecycle/extensions/src/test/java/androidx/lifecycle/ViewModelProvidersTest.java b/lifecycle/extensions/src/test/java/androidx/lifecycle/ViewModelProvidersTest.java
new file mode 100644
index 0000000..e21f5c1
--- /dev/null
+++ b/lifecycle/extensions/src/test/java/androidx/lifecycle/ViewModelProvidersTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 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 androidx.lifecycle;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ViewModelProvidersTest {
+
+ @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());
+ }
+}
diff --git a/lifecycle/integration-tests/test-app/src/main/java/com/android/support/lifecycle/testapp/LiveDataTestActivity.java b/lifecycle/integration-tests/test-app/src/main/java/com/android/support/lifecycle/testapp/LiveDataTestActivity.java
index 5bbd368..7b94407 100644
--- a/lifecycle/integration-tests/test-app/src/main/java/com/android/support/lifecycle/testapp/LiveDataTestActivity.java
+++ b/lifecycle/integration-tests/test-app/src/main/java/com/android/support/lifecycle/testapp/LiveDataTestActivity.java
@@ -17,7 +17,8 @@
package com.android.support.lifecycle.testapp;
import android.os.Bundle;
-import android.support.annotation.Nullable;
+
+import androidx.annotation.Nullable;
import com.android.support.lifecycle.LifecycleActivity;
import com.android.support.lifecycle.LifecycleFragment;
diff --git a/lifecycle/integration-tests/testapp/build.gradle b/lifecycle/integration-tests/testapp/build.gradle
index c8fba58..a842c45 100644
--- a/lifecycle/integration-tests/testapp/build.gradle
+++ b/lifecycle/integration-tests/testapp/build.gradle
@@ -22,7 +22,7 @@
android {
defaultConfig {
- applicationId "android.arch.lifecycle.testapp"
+ applicationId "androidx.lifecycle.testapp"
}
buildTypes {
// test coverage does not work w/ jack
@@ -37,18 +37,18 @@
dependencies {
// IJ canont figure out transitive dependencies so need to declare them.
- implementation(project(":lifecycle:common"))
- implementation(project(":lifecycle:runtime"))
- implementation(project(":lifecycle:extensions"))
- annotationProcessor(project(":lifecycle:compiler"))
+ implementation(project(":lifecycle:lifecycle-common"))
+ implementation(project(":lifecycle:lifecycle-runtime"))
+ implementation(project(":lifecycle:lifecycle-extensions"))
+ annotationProcessor(project(":lifecycle:lifecycle-compiler"))
- androidTestAnnotationProcessor(project(":lifecycle:compiler"))
+ androidTestAnnotationProcessor(project(":lifecycle:lifecycle-compiler"))
androidTestImplementation(TEST_RUNNER)
androidTestImplementation(ESPRESSO_CORE)
testImplementation(JUNIT)
testImplementation(MOCKITO_CORE)
- testAnnotationProcessor(project(":lifecycle:compiler"))
+ testAnnotationProcessor(project(":lifecycle:lifecycle-compiler"))
}
tasks['check'].dependsOn(tasks['connectedCheck'])
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/ActivityFullLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/ActivityFullLifecycleTest.java
deleted file mode 100644
index 78dd015..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/ActivityFullLifecycleTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.TestUtils.OrderedTuples.CREATE;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.DESTROY;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.PAUSE;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.RESUME;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.START;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.STOP;
-import static android.arch.lifecycle.TestUtils.flatMap;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import android.app.Activity;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.testapp.CollectingLifecycleOwner;
-import android.arch.lifecycle.testapp.CollectingSupportActivity;
-import android.arch.lifecycle.testapp.FrameworkLifecycleRegistryActivity;
-import android.arch.lifecycle.testapp.TestEvent;
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v4.util.Pair;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.List;
-
-@SmallTest
-@RunWith(Parameterized.class)
-public class ActivityFullLifecycleTest {
- @Rule
- public final ActivityTestRule<? extends CollectingLifecycleOwner> activityTestRule;
-
- @Parameterized.Parameters
- public static Class[] params() {
- return new Class[]{CollectingSupportActivity.class,
- FrameworkLifecycleRegistryActivity.class};
- }
-
- public ActivityFullLifecycleTest(Class<? extends Activity> activityClass) {
- //noinspection unchecked
- activityTestRule = new ActivityTestRule(activityClass);
- }
-
-
- @Test
- public void testFullLifecycle() throws Throwable {
- CollectingLifecycleOwner owner = activityTestRule.getActivity();
- TestUtils.waitTillResumed(owner, activityTestRule);
- activityTestRule.finishActivity();
-
- TestUtils.waitTillDestroyed(owner, activityTestRule);
- List<Pair<TestEvent, Event>> results = owner.copyCollectedEvents();
- assertThat(results, is(flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY)));
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java
deleted file mode 100644
index f48f788..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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.TestUtils.recreateActivity;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.iterableWithSize;
-
-import static java.util.Arrays.asList;
-
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.testapp.EmptyActivity;
-import android.arch.lifecycle.testapp.R;
-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;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class FragmentInBackStackLifecycleTest {
- @Rule
- public ActivityTestRule<EmptyActivity> activityTestRule = new ActivityTestRule<>(
- EmptyActivity.class);
-
- @Test
- public void test() throws Throwable {
- final ArrayList<Event> collectedEvents = new ArrayList<>();
- LifecycleObserver collectingObserver = new LifecycleObserver() {
- @OnLifecycleEvent(Event.ON_ANY)
- void onAny(@SuppressWarnings("unused") LifecycleOwner owner, Event event) {
- collectedEvents.add(event);
- }
- };
- final FragmentActivity activity = activityTestRule.getActivity();
- activityTestRule.runOnUiThread(() -> {
- FragmentManager fm = activity.getSupportFragmentManager();
- Fragment fragment = new Fragment();
- fm.beginTransaction().add(R.id.fragment_container, fragment, "tag").addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- fragment.getLifecycle().addObserver(collectingObserver);
- Fragment fragment2 = new Fragment();
- fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- assertThat(collectedEvents, is(asList(ON_CREATE, ON_START, ON_RESUME,
- ON_PAUSE, ON_STOP)));
- collectedEvents.clear();
- });
- 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();
- Fragment fragment = fm.findFragmentByTag("tag");
- fragment.getLifecycle().addObserver(collectingObserver);
- assertThat(collectedEvents, iterableWithSize(0));
- fm.popBackStackImmediate();
- assertThat(collectedEvents, is(asList(ON_CREATE, ON_START, ON_RESUME)));
- });
- }
-
-
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java
deleted file mode 100644
index d5ba9df..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.app.Instrumentation;
-import android.arch.lifecycle.testapp.CollectingSupportActivity;
-import android.arch.lifecycle.testapp.CollectingSupportFragment;
-import android.arch.lifecycle.testapp.NavigationDialogActivity;
-import android.content.Intent;
-import android.os.Build;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SdkSuppress;
-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.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LiveDataOnSaveInstanceStateTest {
- @Rule
- public ActivityTestRule<CollectingSupportActivity> mActivityTestRule =
- new ActivityTestRule<>(CollectingSupportActivity.class);
-
- @Test
- @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
- public void liveData_partiallyObscuredActivity_maxSdkM() throws Throwable {
- CollectingSupportActivity activity = mActivityTestRule.getActivity();
-
- liveData_partiallyObscuredLifecycleOwner_maxSdkM(activity);
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
- public void liveData_partiallyObscuredActivityWithFragment_maxSdkM() throws Throwable {
- CollectingSupportActivity activity = mActivityTestRule.getActivity();
- CollectingSupportFragment fragment = new CollectingSupportFragment();
- mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment));
-
- liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment);
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
- public void liveData_partiallyObscuredActivityFragmentInFragment_maxSdkM() throws Throwable {
- CollectingSupportActivity activity = mActivityTestRule.getActivity();
- CollectingSupportFragment fragment = new CollectingSupportFragment();
- CollectingSupportFragment fragment2 = new CollectingSupportFragment();
- mActivityTestRule.runOnUiThread(() -> {
- activity.replaceFragment(fragment);
- fragment.replaceFragment(fragment2);
- });
-
- liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment2);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- public void liveData_partiallyObscuredActivity_minSdkN() throws Throwable {
- CollectingSupportActivity activity = mActivityTestRule.getActivity();
-
- liveData_partiallyObscuredLifecycleOwner_minSdkN(activity);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- public void liveData_partiallyObscuredActivityWithFragment_minSdkN() throws Throwable {
- CollectingSupportActivity activity = mActivityTestRule.getActivity();
- CollectingSupportFragment fragment = new CollectingSupportFragment();
- mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment));
-
- liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- public void liveData_partiallyObscuredActivityFragmentInFragment_minSdkN() throws Throwable {
- CollectingSupportActivity activity = mActivityTestRule.getActivity();
- CollectingSupportFragment fragment = new CollectingSupportFragment();
- CollectingSupportFragment fragment2 = new CollectingSupportFragment();
- mActivityTestRule.runOnUiThread(() -> {
- activity.replaceFragment(fragment);
- fragment.replaceFragment(fragment2);
- });
-
- liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment2);
- }
-
- private void liveData_partiallyObscuredLifecycleOwner_maxSdkM(LifecycleOwner lifecycleOwner)
- throws Throwable {
- final AtomicInteger atomicInteger = new AtomicInteger(0);
- MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>();
- mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0));
-
- TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule);
- mActivityTestRule.runOnUiThread(() ->
- mutableLiveData.observe(lifecycleOwner, atomicInteger::set));
-
- final FragmentActivity dialogActivity = launchDialog();
-
- TestUtils.waitTillCreated(lifecycleOwner, mActivityTestRule);
-
- // Change the LiveData value and assert that the observer is not called given that the
- // lifecycle is in the CREATED state.
- mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1));
- assertThat(atomicInteger.get(), is(0));
-
- // Finish the dialog Activity, wait for the main activity to be resumed, and assert that
- // the observer's onChanged method is called.
- mActivityTestRule.runOnUiThread(dialogActivity::finish);
- TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule);
- assertThat(atomicInteger.get(), is(1));
- }
-
- private void liveData_partiallyObscuredLifecycleOwner_minSdkN(LifecycleOwner lifecycleOwner)
- throws Throwable {
- final AtomicInteger atomicInteger = new AtomicInteger(0);
- MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>();
- mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0));
-
- TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule);
-
- mActivityTestRule.runOnUiThread(() ->
- mutableLiveData.observe(lifecycleOwner, atomicInteger::set));
-
- // Launch the NavigationDialogActivity, partially obscuring the activity, and wait for the
- // lifecycleOwner to hit onPause (or enter the STARTED state). On API 24 and above, this
- // onPause should be the last lifecycle method called (and the STARTED state should be the
- // final resting state).
- launchDialog();
- TestUtils.waitTillStarted(lifecycleOwner, mActivityTestRule);
-
- // Change the LiveData's value and verify that the observer's onChanged method is called
- // since we are in the STARTED state.
- mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1));
- assertThat(atomicInteger.get(), is(1));
- }
-
- private FragmentActivity launchDialog() throws Throwable {
- Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
- NavigationDialogActivity.class.getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
-
- FragmentActivity activity = mActivityTestRule.getActivity();
- // helps with less flaky API 16 tests
- Intent intent = new Intent(activity, NavigationDialogActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- activity.startActivity(intent);
- FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity();
- TestUtils.waitTillResumed(fragmentActivity, mActivityTestRule);
- return fragmentActivity;
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/PartiallyCoveredActivityTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/PartiallyCoveredActivityTest.java
deleted file mode 100644
index 07a9dc5..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/PartiallyCoveredActivityTest.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * 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_RESUME;
-import static android.arch.lifecycle.Lifecycle.Event.ON_START;
-import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.CREATE;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.DESTROY;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.PAUSE;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.RESUME;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.START;
-import static android.arch.lifecycle.TestUtils.OrderedTuples.STOP;
-import static android.arch.lifecycle.TestUtils.flatMap;
-import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT;
-import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-
-import android.app.Instrumentation;
-import android.arch.lifecycle.testapp.CollectingLifecycleOwner;
-import android.arch.lifecycle.testapp.CollectingSupportActivity;
-import android.arch.lifecycle.testapp.CollectingSupportFragment;
-import android.arch.lifecycle.testapp.NavigationDialogActivity;
-import android.arch.lifecycle.testapp.TestEvent;
-import android.content.Intent;
-import android.os.Build;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.util.Pair;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-/**
- * Runs tests about the state when an activity is partially covered by another activity. Pre
- * API 24, framework behavior changes so the test rely on whether state is saved or not and makes
- * assertions accordingly.
- */
-@SuppressWarnings("unchecked")
-@RunWith(Parameterized.class)
-@LargeTest
-public class PartiallyCoveredActivityTest {
- private static final List[] IF_SAVED = new List[]{
- // when overlaid
- flatMap(CREATE, START, RESUME, PAUSE,
- singletonList(new Pair<>(LIFECYCLE_EVENT, ON_STOP))),
- // post dialog dismiss
- asList(new Pair<>(OWNER_CALLBACK, ON_RESUME),
- new Pair<>(LIFECYCLE_EVENT, ON_START),
- new Pair<>(LIFECYCLE_EVENT, ON_RESUME)),
- // post finish
- flatMap(PAUSE, STOP, DESTROY)};
-
- private static final List[] IF_NOT_SAVED = new List[]{
- // when overlaid
- flatMap(CREATE, START, RESUME, PAUSE),
- // post dialog dismiss
- flatMap(RESUME),
- // post finish
- flatMap(PAUSE, STOP, DESTROY)};
-
- private static final boolean sShouldSave = Build.VERSION.SDK_INT < Build.VERSION_CODES.N;
- private static final List<Pair<TestEvent, Lifecycle.Event>>[] EXPECTED =
- sShouldSave ? IF_SAVED : IF_NOT_SAVED;
-
- @Rule
- public ActivityTestRule<CollectingSupportActivity> activityRule =
- new ActivityTestRule<CollectingSupportActivity>(
- CollectingSupportActivity.class) {
- @Override
- protected Intent getActivityIntent() {
- // helps with less flaky API 16 tests
- Intent intent = new Intent(InstrumentationRegistry.getTargetContext(),
- CollectingSupportActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- return intent;
- }
- };
- private final boolean mDismissDialog;
-
- @Parameterized.Parameters(name = "dismissDialog_{0}")
- public static List<Boolean> dismissDialog() {
- return asList(true, false);
- }
-
- public PartiallyCoveredActivityTest(boolean dismissDialog) {
- mDismissDialog = dismissDialog;
- }
-
- @Test
- public void coveredWithDialog_activity() throws Throwable {
- final CollectingSupportActivity activity = activityRule.getActivity();
- runTest(activity);
- }
-
- @Test
- public void coveredWithDialog_fragment() throws Throwable {
- CollectingSupportFragment fragment = new CollectingSupportFragment();
- activityRule.runOnUiThread(() -> activityRule.getActivity().replaceFragment(fragment));
- runTest(fragment);
- }
-
- @Test
- public void coveredWithDialog_childFragment() throws Throwable {
- CollectingSupportFragment parentFragment = new CollectingSupportFragment();
- CollectingSupportFragment childFragment = new CollectingSupportFragment();
- activityRule.runOnUiThread(() -> {
- activityRule.getActivity().replaceFragment(parentFragment);
- parentFragment.replaceFragment(childFragment);
- });
- runTest(childFragment);
- }
-
- private void runTest(CollectingLifecycleOwner owner) throws Throwable {
- TestUtils.waitTillResumed(owner, activityRule);
- FragmentActivity dialog = launchDialog();
- assertStateSaving();
- waitForIdle();
- assertThat(owner.copyCollectedEvents(), is(EXPECTED[0]));
- List<Pair<TestEvent, Lifecycle.Event>> expected;
- if (mDismissDialog) {
- dialog.finish();
- TestUtils.waitTillResumed(activityRule.getActivity(), activityRule);
- assertThat(owner.copyCollectedEvents(), is(flatMap(EXPECTED[0], EXPECTED[1])));
- expected = flatMap(EXPECTED[0], EXPECTED[1], EXPECTED[2]);
- } else {
- expected = flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY);
- }
- CollectingSupportActivity activity = activityRule.getActivity();
- activityRule.finishActivity();
- TestUtils.waitTillDestroyed(activity, activityRule);
- assertThat(owner.copyCollectedEvents(), is(expected));
- }
-
- // test sanity
- private void assertStateSaving() throws ExecutionException, InterruptedException {
- final CollectingSupportActivity activity = activityRule.getActivity();
- if (sShouldSave) {
- // state should be saved. wait for it to be saved
- assertThat("test sanity",
- activity.waitForStateSave(20), is(true));
- assertThat("test sanity", activity.getSupportFragmentManager()
- .isStateSaved(), is(true));
- } else {
- // should should not be saved
- assertThat("test sanity", activity.getSupportFragmentManager()
- .isStateSaved(), is(false));
- }
- }
-
- private void waitForIdle() {
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- }
-
- private FragmentActivity launchDialog() throws Throwable {
- Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
- NavigationDialogActivity.class.getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
-
- FragmentActivity activity = activityRule.getActivity();
-
- Intent intent = new Intent(activity, NavigationDialogActivity.class);
- // disabling animations helps with less flaky API 16 tests
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- activity.startActivity(intent);
- FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity();
- TestUtils.waitTillResumed(fragmentActivity, activityRule);
- return fragmentActivity;
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/ProcessOwnerTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/ProcessOwnerTest.java
deleted file mode 100644
index 77baf94..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/ProcessOwnerTest.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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_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.TestUtils.waitTillResumed;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-
-import android.app.Instrumentation;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.testapp.NavigationDialogActivity;
-import android.arch.lifecycle.testapp.NavigationTestActivityFirst;
-import android.arch.lifecycle.testapp.NavigationTestActivitySecond;
-import android.arch.lifecycle.testapp.NonSupportActivity;
-import android.content.Context;
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
-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;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ProcessOwnerTest {
-
- @Rule
- public ActivityTestRule<NavigationTestActivityFirst> activityTestRule =
- new ActivityTestRule<>(NavigationTestActivityFirst.class);
-
- static class ProcessObserver implements LifecycleObserver {
- volatile boolean mChangedState;
-
- @OnLifecycleEvent(Event.ON_ANY)
- void onEvent() {
- mChangedState = true;
- }
- }
-
- private ProcessObserver mObserver = new ProcessObserver();
-
- @After
- public void tearDown() {
- try {
- // reassure that our observer is removed.
- removeProcessObserver(mObserver);
- } catch (Throwable throwable) {
- throwable.printStackTrace();
- }
- }
-
- @Test
- public void testNavigation() throws Throwable {
- FragmentActivity firstActivity = setupObserverOnResume();
- Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
- NavigationTestActivitySecond.class.getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
-
- Intent intent = new Intent(firstActivity, NavigationTestActivitySecond.class);
- firstActivity.finish();
- firstActivity.startActivity(intent);
-
- FragmentActivity secondActivity = (FragmentActivity) monitor.waitForActivity();
- assertThat("Failed to navigate", secondActivity, notNullValue());
- checkProcessObserverSilent(secondActivity);
- }
-
- @Test
- public void testNavigationToNonSupport() throws Throwable {
- FragmentActivity firstActivity = setupObserverOnResume();
- Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
- NonSupportActivity.class.getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
-
- Intent intent = new Intent(firstActivity, NonSupportActivity.class);
- firstActivity.finish();
- firstActivity.startActivity(intent);
- NonSupportActivity secondActivity = (NonSupportActivity) monitor.waitForActivity();
- assertThat("Failed to navigate", secondActivity, notNullValue());
- checkProcessObserverSilent(secondActivity);
- }
-
- @Test
- public void testRecreation() throws Throwable {
- FragmentActivity activity = setupObserverOnResume();
- FragmentActivity recreated = TestUtils.recreateActivity(activity, activityTestRule);
- assertThat("Failed to recreate", recreated, notNullValue());
- checkProcessObserverSilent(recreated);
- }
-
- @Test
- public void testPressHomeButton() throws Throwable {
- setupObserverOnResume();
-
- Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
- NavigationDialogActivity.class.getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
-
- NavigationTestActivityFirst activity = activityTestRule.getActivity();
- activity.startActivity(new Intent(activity, NavigationDialogActivity.class));
- 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(@SuppressWarnings("unused") LifecycleOwner provider,
- Event event) {
- events.add(event);
- }
- };
- addProcessObserver(collectingObserver);
- events.clear();
- assertThat(activity.moveTaskToBack(true), is(true));
- Thread.sleep(ProcessLifecycleOwner.TIMEOUT_MS * 2);
- assertThat(events.toArray(), is(new Event[]{ON_PAUSE, ON_STOP}));
- events.clear();
- Context context = InstrumentationRegistry.getContext();
- context.startActivity(new Intent(activity, NavigationDialogActivity.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- waitTillResumed(dialogActivity, activityTestRule);
- assertThat(events.toArray(), is(new Event[]{ON_START, ON_RESUME}));
- removeProcessObserver(collectingObserver);
- dialogActivity.finish();
- }
-
- private FragmentActivity setupObserverOnResume() throws Throwable {
- FragmentActivity firstActivity = activityTestRule.getActivity();
- waitTillResumed(firstActivity, activityTestRule);
- addProcessObserver(mObserver);
- mObserver.mChangedState = false;
- return firstActivity;
- }
-
- private void addProcessObserver(LifecycleObserver observer) throws Throwable {
- activityTestRule.runOnUiThread(() ->
- ProcessLifecycleOwner.get().getLifecycle().addObserver(observer));
- }
-
- private void removeProcessObserver(LifecycleObserver observer) throws Throwable {
- activityTestRule.runOnUiThread(() ->
- ProcessLifecycleOwner.get().getLifecycle().removeObserver(observer));
- }
-
- private void checkProcessObserverSilent(FragmentActivity activity) throws Throwable {
- waitTillResumed(activity, activityTestRule);
- assertThat(mObserver.mChangedState, is(false));
- activityTestRule.runOnUiThread(() ->
- ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver));
- }
-
- private void checkProcessObserverSilent(NonSupportActivity activity) throws Throwable {
- assertThat(activity.awaitResumedState(), is(true));
- assertThat(mObserver.mChangedState, is(false));
- activityTestRule.runOnUiThread(() ->
- ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver));
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/SimpleAppFullLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/SimpleAppFullLifecycleTest.java
deleted file mode 100644
index 44d1e8a..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/SimpleAppFullLifecycleTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.State.CREATED;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.isIn;
-import static org.hamcrest.Matchers.notNullValue;
-
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.Lifecycle.State;
-import android.arch.lifecycle.testapp.SimpleAppLifecycleTestActivity;
-import android.arch.lifecycle.testapp.SimpleAppLifecycleTestActivity.TestEventType;
-import android.support.test.filters.LargeTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.util.Pair;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class SimpleAppFullLifecycleTest {
-
- @SuppressWarnings("unchecked")
- private static final Pair[] EXPECTED_EVENTS_CONSTRUCTION =
- new Pair[] {
- new Pair(TestEventType.PROCESS_EVENT, Event.ON_CREATE),
- new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_CREATE),
- new Pair(TestEventType.PROCESS_EVENT, Event.ON_START),
- new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_START),
- new Pair(TestEventType.PROCESS_EVENT, Event.ON_RESUME),
- new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_RESUME),
- };
-
- @SuppressWarnings("unchecked")
- private static final Pair[] EXPECTED_EVENTS_DESTRUCTION =
- new Pair[]{
-
- new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_PAUSE),
- new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_STOP),
- new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_DESTROY),
-
- new Pair(TestEventType.PROCESS_EVENT, Event.ON_PAUSE),
- new Pair(TestEventType.PROCESS_EVENT, Event.ON_STOP),
- };
- @Rule
- public ActivityTestRule<SimpleAppLifecycleTestActivity> activityTestRule =
- new ActivityTestRule<>(SimpleAppLifecycleTestActivity.class, false, false);
-
- @Before
- public void setup() {
- // cool down period, so application state will become DESTROYED
- try {
- Thread.sleep(ProcessLifecycleOwner.TIMEOUT_MS * 2);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- SimpleAppLifecycleTestActivity.startProcessObserver();
- }
-
- @After
- public void tearDown() {
- SimpleAppLifecycleTestActivity.stopProcessObserver();
- }
-
- @Test
- public void testFullLifecycle() throws InterruptedException {
- State currentState = ProcessLifecycleOwner.get().getLifecycle().getCurrentState();
- assertThat(currentState, is(CREATED));
- activityTestRule.launchActivity(null);
- List<Pair<TestEventType, Event>> events = SimpleAppLifecycleTestActivity.awaitForEvents();
- assertThat("Failed to await for events", events, notNullValue());
- //noinspection ConstantConditions
- assertThat(events.subList(0, 6).toArray(), is(EXPECTED_EVENTS_CONSTRUCTION));
-
- // TODO: bug 35122523
- for (Pair<TestEventType, Event> event: events.subList(6, 11)) {
- assertThat(event, isIn(EXPECTED_EVENTS_DESTRUCTION));
- }
- }
-
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/SynchronousActivityLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/SynchronousActivityLifecycleTest.java
deleted file mode 100644
index 4b91363..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/SynchronousActivityLifecycleTest.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-import android.app.Activity;
-import android.app.Application;
-import android.app.Instrumentation;
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.testapp.LifecycleTestActivity;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.os.Build;
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.UiThreadTestRule;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.reflect.Method;
-
-/**
- * It tests that an event is dispatched immediately after a call of corresponding OnXXX method
- * during an execution of performXXX
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class SynchronousActivityLifecycleTest {
-
- @Rule
- public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();
-
- @Test
- public void testOnCreateCall() throws Throwable {
- testSynchronousCall(Event.ON_CREATE,
- activity -> {
- },
- activity -> getInstrumentation().callActivityOnCreate(activity, null));
- }
-
- @Test
- public void testOnStartCall() throws Throwable {
- testSynchronousCall(Lifecycle.Event.ON_START,
- activity -> getInstrumentation().callActivityOnCreate(activity, null),
- SynchronousActivityLifecycleTest::performStart);
- }
-
- @Test
- public void testOnResumeCall() throws Throwable {
- testSynchronousCall(Lifecycle.Event.ON_RESUME,
- activity -> {
- getInstrumentation().callActivityOnCreate(activity, null);
- performStart(activity);
- },
- SynchronousActivityLifecycleTest::performResume);
- }
-
- @Test
- public void testOnStopCall() throws Throwable {
- testSynchronousCall(Lifecycle.Event.ON_STOP,
- activity -> {
- getInstrumentation().callActivityOnCreate(activity, null);
- performStart(activity);
- },
- SynchronousActivityLifecycleTest::performStop);
- }
-
- @Test
- public void testOnDestroyCall() throws Throwable {
- testSynchronousCall(Lifecycle.Event.ON_DESTROY,
- activity -> getInstrumentation().callActivityOnCreate(activity, null),
- activity -> getInstrumentation().callActivityOnDestroy(activity));
- }
-
- public void testSynchronousCall(Event event, ActivityCall preInit, ActivityCall call)
- throws Throwable {
- uiThreadTestRule.runOnUiThread(() -> {
- Intent intent = new Intent();
- ComponentName cn = new ComponentName(LifecycleTestActivity.class.getPackage().getName(),
- LifecycleTestActivity.class.getName());
- intent.setComponent(cn);
- Instrumentation instrumentation = getInstrumentation();
- try {
- Application app =
- (Application) instrumentation.getTargetContext().getApplicationContext();
- LifecycleTestActivity testActivity =
- (LifecycleTestActivity) instrumentation.newActivity(
- LifecycleTestActivity.class, instrumentation.getTargetContext(),
- null, app, intent, new ActivityInfo(), "bla", null, null, null);
- preInit.call(testActivity);
- TestObserver testObserver = new TestObserver(testActivity, event);
- testActivity.getLifecycle().addObserver(testObserver);
- testObserver.unmute();
- call.call(testActivity);
-
- assertThat(testObserver.mEventReceived, is(true));
- } catch (Exception e) {
- throw new Error(e);
- }
- });
- }
-
- // Instrumentation.callOnActivityCreate calls performCreate on mActivity,
- // but Instrumentation.callOnActivityStart calls onStart instead of performStart. ¯\_(ツ)_/¯
- private static void performStart(Activity activity) {
- try {
- Method m = Activity.class.getDeclaredMethod("performStart");
- m.setAccessible(true);
- m.invoke(activity);
- } catch (Exception e) {
- throw new Error(e);
- }
- }
-
- private static void performResume(Activity activity) {
- try {
- Method m = Activity.class.getDeclaredMethod("performResume");
- m.setAccessible(true);
- m.invoke(activity);
- } catch (Exception e) {
- throw new Error(e);
- }
- }
-
-
- private static void performStop(Activity activity) {
- try {
- if (Build.VERSION.SDK_INT >= 24) {
- Method m = Activity.class.getDeclaredMethod("performStop", boolean.class);
- m.setAccessible(true);
- m.invoke(activity, false);
- } else {
- Method m = Activity.class.getDeclaredMethod("performStop");
- m.setAccessible(true);
- m.invoke(activity);
- }
- } catch (Exception e) {
- throw new Error(e);
- }
- }
-
- private static class TestObserver implements GenericLifecycleObserver {
- private final LifecycleTestActivity mActivity;
- private final Event mExpectedEvent;
- boolean mEventReceived = false;
- boolean mMuted = true;
-
- private TestObserver(LifecycleTestActivity activity, Event expectedEvent) {
- this.mActivity = activity;
- this.mExpectedEvent = expectedEvent;
- }
-
- void unmute() {
- mMuted = false;
- }
-
- @Override
- public void onStateChanged(LifecycleOwner lifecycleOwner, Event event) {
- if (mMuted) {
- return;
- }
- assertThat(event, is(mExpectedEvent));
- assertThat(mActivity.mLifecycleCallFinished, is(true));
- mEventReceived = true;
- }
- }
-
- private interface ActivityCall {
- void call(Activity activity);
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/TestUtils.java b/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/TestUtils.java
deleted file mode 100644
index f7f9bbe..0000000
--- a/lifecycle/integration-tests/testapp/src/androidTest/java/android/arch/lifecycle/TestUtils.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * 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.DESTROYED;
-import static android.arch.lifecycle.Lifecycle.State.RESUMED;
-import static android.arch.lifecycle.Lifecycle.State.STARTED;
-import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT;
-import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.app.Activity;
-import android.app.Instrumentation;
-import android.app.Instrumentation.ActivityMonitor;
-import android.arch.lifecycle.testapp.TestEvent;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.v4.util.Pair;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-class TestUtils {
-
- private static final long TIMEOUT_MS = 2000;
-
- @SuppressWarnings("unchecked")
- static <T extends Activity> T recreateActivity(final T activity, ActivityTestRule rule)
- throws Throwable {
- ActivityMonitor monitor = new ActivityMonitor(
- activity.getClass().getCanonicalName(), null, false);
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- instrumentation.addMonitor(monitor);
- rule.runOnUiThread(activity::recreate);
- T result;
-
- // this guarantee that we will reinstall monitor between notifications about onDestroy
- // and onCreate
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
- synchronized (monitor) {
- do {
- // the documetation says "Block until an Activity is created
- // that matches this monitor." This statement is true, but there are some other
- // true statements like: "Block until an Activity is destoyed" or
- // "Block until an Activity is resumed"...
-
- // this call will release synchronization monitor's monitor
- result = (T) monitor.waitForActivityWithTimeout(TIMEOUT_MS);
- if (result == null) {
- throw new RuntimeException("Timeout. Failed to recreate an activity");
- }
- } while (result == activity);
- }
- return result;
- }
-
- static void waitTillCreated(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
- throws Throwable {
- waitTillState(owner, activityRule, CREATED);
- }
-
- static void waitTillStarted(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
- throws Throwable {
- waitTillState(owner, activityRule, STARTED);
- }
-
- static void waitTillResumed(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
- throws Throwable {
- waitTillState(owner, activityRule, RESUMED);
- }
-
- static void waitTillDestroyed(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
- throws Throwable {
- waitTillState(owner, activityRule, DESTROYED);
- }
-
- static void waitTillState(final LifecycleOwner owner, ActivityTestRule<?> activityRule,
- Lifecycle.State state)
- throws Throwable {
- final CountDownLatch latch = new CountDownLatch(1);
- activityRule.runOnUiThread(() -> {
- Lifecycle.State currentState = owner.getLifecycle().getCurrentState();
- if (currentState == state) {
- latch.countDown();
- } else {
- owner.getLifecycle().addObserver(new LifecycleObserver() {
- @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- public void onStateChanged(LifecycleOwner provider) {
- if (provider.getLifecycle().getCurrentState() == state) {
- latch.countDown();
- provider.getLifecycle().removeObserver(this);
- }
- }
- });
- }
- });
- boolean latchResult = latch.await(1, TimeUnit.MINUTES);
- assertThat("expected " + state + " never happened. Current state:"
- + owner.getLifecycle().getCurrentState(), latchResult, is(true));
-
- // wait for another loop to ensure all observers are called
- activityRule.runOnUiThread(() -> {
- // do nothing
- });
- }
-
- @SafeVarargs
- static <T> List<T> flatMap(List<T>... items) {
- ArrayList<T> result = new ArrayList<>();
- for (List<T> item : items) {
- result.addAll(item);
- }
- return result;
- }
-
- /**
- * Event tuples of {@link TestEvent} and {@link Lifecycle.Event}
- * in the order they should arrive.
- */
- @SuppressWarnings("unchecked")
- static class OrderedTuples {
- static final List<Pair<TestEvent, Lifecycle.Event>> CREATE =
- Arrays.asList(new Pair(OWNER_CALLBACK, ON_CREATE),
- new Pair(LIFECYCLE_EVENT, ON_CREATE));
- static final List<Pair<TestEvent, Lifecycle.Event>> START =
- Arrays.asList(new Pair(OWNER_CALLBACK, ON_START),
- new Pair(LIFECYCLE_EVENT, ON_START));
- static final List<Pair<TestEvent, Lifecycle.Event>> RESUME =
- Arrays.asList(new Pair(OWNER_CALLBACK, ON_RESUME),
- new Pair(LIFECYCLE_EVENT, ON_RESUME));
- static final List<Pair<TestEvent, Lifecycle.Event>> PAUSE =
- Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_PAUSE),
- new Pair(OWNER_CALLBACK, ON_PAUSE));
- static final List<Pair<TestEvent, Lifecycle.Event>> STOP =
- Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_STOP),
- new Pair(OWNER_CALLBACK, ON_STOP));
- static final List<Pair<TestEvent, Lifecycle.Event>> DESTROY =
- Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_DESTROY),
- new Pair(OWNER_CALLBACK, ON_DESTROY));
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/ActivityFullLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/ActivityFullLifecycleTest.java
new file mode 100644
index 0000000..a5a6f0f
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/ActivityFullLifecycleTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.TestUtils.OrderedTuples.CREATE;
+import static androidx.lifecycle.TestUtils.OrderedTuples.DESTROY;
+import static androidx.lifecycle.TestUtils.OrderedTuples.PAUSE;
+import static androidx.lifecycle.TestUtils.OrderedTuples.RESUME;
+import static androidx.lifecycle.TestUtils.OrderedTuples.START;
+import static androidx.lifecycle.TestUtils.OrderedTuples.STOP;
+import static androidx.lifecycle.TestUtils.flatMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import android.app.Activity;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.testapp.CollectingLifecycleOwner;
+import androidx.lifecycle.testapp.CollectingSupportActivity;
+import androidx.lifecycle.testapp.FrameworkLifecycleRegistryActivity;
+import androidx.lifecycle.testapp.TestEvent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class ActivityFullLifecycleTest {
+ @Rule
+ public final ActivityTestRule<? extends CollectingLifecycleOwner> activityTestRule;
+
+ @Parameterized.Parameters
+ public static Class[] params() {
+ return new Class[]{CollectingSupportActivity.class,
+ FrameworkLifecycleRegistryActivity.class};
+ }
+
+ public ActivityFullLifecycleTest(Class<? extends Activity> activityClass) {
+ //noinspection unchecked
+ activityTestRule = new ActivityTestRule(activityClass);
+ }
+
+
+ @Test
+ public void testFullLifecycle() throws Throwable {
+ CollectingLifecycleOwner owner = activityTestRule.getActivity();
+ TestUtils.waitTillResumed(owner, activityTestRule);
+ activityTestRule.finishActivity();
+
+ TestUtils.waitTillDestroyed(owner, activityTestRule);
+ List<Pair<TestEvent, Event>> results = owner.copyCollectedEvents();
+ assertThat(results, is(flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY)));
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/FragmentInBackStackLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/FragmentInBackStackLifecycleTest.java
new file mode 100644
index 0000000..e20d95c
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/FragmentInBackStackLifecycleTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 androidx.lifecycle;
+
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.TestUtils.recreateActivity;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.iterableWithSize;
+
+import static java.util.Arrays.asList;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.testapp.EmptyActivity;
+import androidx.lifecycle.testapp.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class FragmentInBackStackLifecycleTest {
+ @Rule
+ public ActivityTestRule<EmptyActivity> activityTestRule = new ActivityTestRule<>(
+ EmptyActivity.class);
+
+ @Test
+ public void test() throws Throwable {
+ final ArrayList<Event> collectedEvents = new ArrayList<>();
+ LifecycleObserver collectingObserver = new LifecycleObserver() {
+ @OnLifecycleEvent(Event.ON_ANY)
+ void onAny(@SuppressWarnings("unused") LifecycleOwner owner, Event event) {
+ collectedEvents.add(event);
+ }
+ };
+ final FragmentActivity activity = activityTestRule.getActivity();
+ activityTestRule.runOnUiThread(() -> {
+ FragmentManager fm = activity.getSupportFragmentManager();
+ Fragment fragment = new Fragment();
+ fm.beginTransaction().add(R.id.fragment_container, fragment, "tag").addToBackStack(null)
+ .commit();
+ fm.executePendingTransactions();
+
+ fragment.getLifecycle().addObserver(collectingObserver);
+ Fragment fragment2 = new Fragment();
+ fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null)
+ .commit();
+ fm.executePendingTransactions();
+ assertThat(collectedEvents, is(asList(ON_CREATE, ON_START, ON_RESUME,
+ ON_PAUSE, ON_STOP)));
+ collectedEvents.clear();
+ });
+ 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();
+ Fragment fragment = fm.findFragmentByTag("tag");
+ fragment.getLifecycle().addObserver(collectingObserver);
+ assertThat(collectedEvents, iterableWithSize(0));
+ fm.popBackStackImmediate();
+ assertThat(collectedEvents, is(asList(ON_CREATE, ON_START, ON_RESUME)));
+ });
+ }
+
+
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/LiveDataOnSaveInstanceStateTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/LiveDataOnSaveInstanceStateTest.java
new file mode 100644
index 0000000..32312c0
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/LiveDataOnSaveInstanceStateTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.testapp.CollectingSupportActivity;
+import androidx.lifecycle.testapp.CollectingSupportFragment;
+import androidx.lifecycle.testapp.NavigationDialogActivity;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LiveDataOnSaveInstanceStateTest {
+ @Rule
+ public ActivityTestRule<CollectingSupportActivity> mActivityTestRule =
+ new ActivityTestRule<>(CollectingSupportActivity.class);
+
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
+ public void liveData_partiallyObscuredActivity_maxSdkM() throws Throwable {
+ CollectingSupportActivity activity = mActivityTestRule.getActivity();
+
+ liveData_partiallyObscuredLifecycleOwner_maxSdkM(activity);
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
+ public void liveData_partiallyObscuredActivityWithFragment_maxSdkM() throws Throwable {
+ CollectingSupportActivity activity = mActivityTestRule.getActivity();
+ CollectingSupportFragment fragment = new CollectingSupportFragment();
+ mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment));
+
+ liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment);
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
+ public void liveData_partiallyObscuredActivityFragmentInFragment_maxSdkM() throws Throwable {
+ CollectingSupportActivity activity = mActivityTestRule.getActivity();
+ CollectingSupportFragment fragment = new CollectingSupportFragment();
+ CollectingSupportFragment fragment2 = new CollectingSupportFragment();
+ mActivityTestRule.runOnUiThread(() -> {
+ activity.replaceFragment(fragment);
+ fragment.replaceFragment(fragment2);
+ });
+
+ liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ public void liveData_partiallyObscuredActivity_minSdkN() throws Throwable {
+ CollectingSupportActivity activity = mActivityTestRule.getActivity();
+
+ liveData_partiallyObscuredLifecycleOwner_minSdkN(activity);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ public void liveData_partiallyObscuredActivityWithFragment_minSdkN() throws Throwable {
+ CollectingSupportActivity activity = mActivityTestRule.getActivity();
+ CollectingSupportFragment fragment = new CollectingSupportFragment();
+ mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment));
+
+ liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ public void liveData_partiallyObscuredActivityFragmentInFragment_minSdkN() throws Throwable {
+ CollectingSupportActivity activity = mActivityTestRule.getActivity();
+ CollectingSupportFragment fragment = new CollectingSupportFragment();
+ CollectingSupportFragment fragment2 = new CollectingSupportFragment();
+ mActivityTestRule.runOnUiThread(() -> {
+ activity.replaceFragment(fragment);
+ fragment.replaceFragment(fragment2);
+ });
+
+ liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment2);
+ }
+
+ private void liveData_partiallyObscuredLifecycleOwner_maxSdkM(LifecycleOwner lifecycleOwner)
+ throws Throwable {
+ final AtomicInteger atomicInteger = new AtomicInteger(0);
+ MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>();
+ mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0));
+
+ TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule);
+ mActivityTestRule.runOnUiThread(() ->
+ mutableLiveData.observe(lifecycleOwner, atomicInteger::set));
+
+ final FragmentActivity dialogActivity = launchDialog();
+
+ TestUtils.waitTillCreated(lifecycleOwner, mActivityTestRule);
+
+ // Change the LiveData value and assert that the observer is not called given that the
+ // lifecycle is in the CREATED state.
+ mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1));
+ assertThat(atomicInteger.get(), is(0));
+
+ // Finish the dialog Activity, wait for the main activity to be resumed, and assert that
+ // the observer's onChanged method is called.
+ mActivityTestRule.runOnUiThread(dialogActivity::finish);
+ TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule);
+ assertThat(atomicInteger.get(), is(1));
+ }
+
+ private void liveData_partiallyObscuredLifecycleOwner_minSdkN(LifecycleOwner lifecycleOwner)
+ throws Throwable {
+ final AtomicInteger atomicInteger = new AtomicInteger(0);
+ MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>();
+ mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0));
+
+ TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule);
+
+ mActivityTestRule.runOnUiThread(() ->
+ mutableLiveData.observe(lifecycleOwner, atomicInteger::set));
+
+ // Launch the NavigationDialogActivity, partially obscuring the activity, and wait for the
+ // lifecycleOwner to hit onPause (or enter the STARTED state). On API 24 and above, this
+ // onPause should be the last lifecycle method called (and the STARTED state should be the
+ // final resting state).
+ launchDialog();
+ TestUtils.waitTillStarted(lifecycleOwner, mActivityTestRule);
+
+ // Change the LiveData's value and verify that the observer's onChanged method is called
+ // since we are in the STARTED state.
+ mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1));
+ assertThat(atomicInteger.get(), is(1));
+ }
+
+ private FragmentActivity launchDialog() throws Throwable {
+ Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+ NavigationDialogActivity.class.getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+
+ FragmentActivity activity = mActivityTestRule.getActivity();
+ // helps with less flaky API 16 tests
+ Intent intent = new Intent(activity, NavigationDialogActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ activity.startActivity(intent);
+ FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity();
+ TestUtils.waitTillResumed(fragmentActivity, mActivityTestRule);
+ return fragmentActivity;
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java
new file mode 100644
index 0000000..7a6d389
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/PartiallyCoveredActivityTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.TestUtils.OrderedTuples.CREATE;
+import static androidx.lifecycle.TestUtils.OrderedTuples.DESTROY;
+import static androidx.lifecycle.TestUtils.OrderedTuples.PAUSE;
+import static androidx.lifecycle.TestUtils.OrderedTuples.RESUME;
+import static androidx.lifecycle.TestUtils.OrderedTuples.START;
+import static androidx.lifecycle.TestUtils.OrderedTuples.STOP;
+import static androidx.lifecycle.TestUtils.flatMap;
+import static androidx.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT;
+import static androidx.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+
+import androidx.core.util.Pair;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.testapp.CollectingLifecycleOwner;
+import androidx.lifecycle.testapp.CollectingSupportActivity;
+import androidx.lifecycle.testapp.CollectingSupportFragment;
+import androidx.lifecycle.testapp.NavigationDialogActivity;
+import androidx.lifecycle.testapp.TestEvent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Runs tests about the state when an activity is partially covered by another activity. Pre
+ * API 24, framework behavior changes so the test rely on whether state is saved or not and makes
+ * assertions accordingly.
+ */
+@SuppressWarnings("unchecked")
+@RunWith(Parameterized.class)
+@LargeTest
+public class PartiallyCoveredActivityTest {
+ private static final List[] IF_SAVED = new List[]{
+ // when overlaid
+ flatMap(CREATE, START, RESUME, PAUSE,
+ singletonList(new Pair<>(LIFECYCLE_EVENT, ON_STOP))),
+ // post dialog dismiss
+ asList(new Pair<>(OWNER_CALLBACK, ON_RESUME),
+ new Pair<>(LIFECYCLE_EVENT, ON_START),
+ new Pair<>(LIFECYCLE_EVENT, ON_RESUME)),
+ // post finish
+ flatMap(PAUSE, STOP, DESTROY)};
+
+ private static final List[] IF_NOT_SAVED = new List[]{
+ // when overlaid
+ flatMap(CREATE, START, RESUME, PAUSE),
+ // post dialog dismiss
+ flatMap(RESUME),
+ // post finish
+ flatMap(PAUSE, STOP, DESTROY)};
+
+ private static final boolean sShouldSave = Build.VERSION.SDK_INT < Build.VERSION_CODES.N;
+ private static final List<Pair<TestEvent, Lifecycle.Event>>[] EXPECTED =
+ sShouldSave ? IF_SAVED : IF_NOT_SAVED;
+
+ @Rule
+ public ActivityTestRule<CollectingSupportActivity> activityRule =
+ new ActivityTestRule<CollectingSupportActivity>(
+ CollectingSupportActivity.class) {
+ @Override
+ protected Intent getActivityIntent() {
+ // helps with less flaky API 16 tests
+ Intent intent = new Intent(InstrumentationRegistry.getTargetContext(),
+ CollectingSupportActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ return intent;
+ }
+ };
+ private final boolean mDismissDialog;
+
+ @Parameterized.Parameters(name = "dismissDialog_{0}")
+ public static List<Boolean> dismissDialog() {
+ return asList(true, false);
+ }
+
+ public PartiallyCoveredActivityTest(boolean dismissDialog) {
+ mDismissDialog = dismissDialog;
+ }
+
+ @Test
+ public void coveredWithDialog_activity() throws Throwable {
+ final CollectingSupportActivity activity = activityRule.getActivity();
+ runTest(activity);
+ }
+
+ @Test
+ public void coveredWithDialog_fragment() throws Throwable {
+ CollectingSupportFragment fragment = new CollectingSupportFragment();
+ activityRule.runOnUiThread(() -> activityRule.getActivity().replaceFragment(fragment));
+ runTest(fragment);
+ }
+
+ @Test
+ public void coveredWithDialog_childFragment() throws Throwable {
+ CollectingSupportFragment parentFragment = new CollectingSupportFragment();
+ CollectingSupportFragment childFragment = new CollectingSupportFragment();
+ activityRule.runOnUiThread(() -> {
+ activityRule.getActivity().replaceFragment(parentFragment);
+ parentFragment.replaceFragment(childFragment);
+ });
+ runTest(childFragment);
+ }
+
+ private void runTest(CollectingLifecycleOwner owner) throws Throwable {
+ TestUtils.waitTillResumed(owner, activityRule);
+ FragmentActivity dialog = launchDialog();
+ assertStateSaving();
+ waitForIdle();
+ assertThat(owner.copyCollectedEvents(), is(EXPECTED[0]));
+ List<Pair<TestEvent, Lifecycle.Event>> expected;
+ if (mDismissDialog) {
+ dialog.finish();
+ TestUtils.waitTillResumed(activityRule.getActivity(), activityRule);
+ assertThat(owner.copyCollectedEvents(), is(flatMap(EXPECTED[0], EXPECTED[1])));
+ expected = flatMap(EXPECTED[0], EXPECTED[1], EXPECTED[2]);
+ } else {
+ expected = flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY);
+ }
+ CollectingSupportActivity activity = activityRule.getActivity();
+ activityRule.finishActivity();
+ TestUtils.waitTillDestroyed(activity, activityRule);
+ assertThat(owner.copyCollectedEvents(), is(expected));
+ }
+
+ // test sanity
+ private void assertStateSaving() throws ExecutionException, InterruptedException {
+ final CollectingSupportActivity activity = activityRule.getActivity();
+ if (sShouldSave) {
+ // state should be saved. wait for it to be saved
+ assertThat("test sanity",
+ activity.waitForStateSave(20), is(true));
+ assertThat("test sanity", activity.getSupportFragmentManager()
+ .isStateSaved(), is(true));
+ } else {
+ // should should not be saved
+ assertThat("test sanity", activity.getSupportFragmentManager()
+ .isStateSaved(), is(false));
+ }
+ }
+
+ private void waitForIdle() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private FragmentActivity launchDialog() throws Throwable {
+ Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+ NavigationDialogActivity.class.getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+
+ FragmentActivity activity = activityRule.getActivity();
+
+ Intent intent = new Intent(activity, NavigationDialogActivity.class);
+ // disabling animations helps with less flaky API 16 tests
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ activity.startActivity(intent);
+ FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity();
+ TestUtils.waitTillResumed(fragmentActivity, activityRule);
+ return fragmentActivity;
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/ProcessOwnerTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/ProcessOwnerTest.java
new file mode 100644
index 0000000..1e27357
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/ProcessOwnerTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.TestUtils.waitTillResumed;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.testapp.NavigationDialogActivity;
+import androidx.lifecycle.testapp.NavigationTestActivityFirst;
+import androidx.lifecycle.testapp.NavigationTestActivitySecond;
+import androidx.lifecycle.testapp.NonSupportActivity;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ProcessOwnerTest {
+
+ @Rule
+ public ActivityTestRule<NavigationTestActivityFirst> activityTestRule =
+ new ActivityTestRule<>(NavigationTestActivityFirst.class);
+
+ static class ProcessObserver implements LifecycleObserver {
+ volatile boolean mChangedState;
+
+ @OnLifecycleEvent(Event.ON_ANY)
+ void onEvent() {
+ mChangedState = true;
+ }
+ }
+
+ private ProcessObserver mObserver = new ProcessObserver();
+
+ @After
+ public void tearDown() {
+ try {
+ // reassure that our observer is removed.
+ removeProcessObserver(mObserver);
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testNavigation() throws Throwable {
+ FragmentActivity firstActivity = setupObserverOnResume();
+ Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+ NavigationTestActivitySecond.class.getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+
+ Intent intent = new Intent(firstActivity, NavigationTestActivitySecond.class);
+ firstActivity.finish();
+ firstActivity.startActivity(intent);
+
+ FragmentActivity secondActivity = (FragmentActivity) monitor.waitForActivity();
+ assertThat("Failed to navigate", secondActivity, notNullValue());
+ checkProcessObserverSilent(secondActivity);
+ }
+
+ @Test
+ public void testNavigationToNonSupport() throws Throwable {
+ FragmentActivity firstActivity = setupObserverOnResume();
+ Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+ NonSupportActivity.class.getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+
+ Intent intent = new Intent(firstActivity, NonSupportActivity.class);
+ firstActivity.finish();
+ firstActivity.startActivity(intent);
+ NonSupportActivity secondActivity = (NonSupportActivity) monitor.waitForActivity();
+ assertThat("Failed to navigate", secondActivity, notNullValue());
+ checkProcessObserverSilent(secondActivity);
+ }
+
+ @Test
+ public void testRecreation() throws Throwable {
+ FragmentActivity activity = setupObserverOnResume();
+ FragmentActivity recreated = TestUtils.recreateActivity(activity, activityTestRule);
+ assertThat("Failed to recreate", recreated, notNullValue());
+ checkProcessObserverSilent(recreated);
+ }
+
+ @Test
+ public void testPressHomeButton() throws Throwable {
+ setupObserverOnResume();
+
+ Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor(
+ NavigationDialogActivity.class.getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+
+ NavigationTestActivityFirst activity = activityTestRule.getActivity();
+ activity.startActivity(new Intent(activity, NavigationDialogActivity.class));
+ 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(@SuppressWarnings("unused") LifecycleOwner provider,
+ Event event) {
+ events.add(event);
+ }
+ };
+ addProcessObserver(collectingObserver);
+ events.clear();
+ assertThat(activity.moveTaskToBack(true), is(true));
+ Thread.sleep(ProcessLifecycleOwner.TIMEOUT_MS * 2);
+ assertThat(events.toArray(), is(new Event[]{ON_PAUSE, ON_STOP}));
+ events.clear();
+ Context context = InstrumentationRegistry.getContext();
+ context.startActivity(new Intent(activity, NavigationDialogActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ waitTillResumed(dialogActivity, activityTestRule);
+ assertThat(events.toArray(), is(new Event[]{ON_START, ON_RESUME}));
+ removeProcessObserver(collectingObserver);
+ dialogActivity.finish();
+ }
+
+ private FragmentActivity setupObserverOnResume() throws Throwable {
+ FragmentActivity firstActivity = activityTestRule.getActivity();
+ waitTillResumed(firstActivity, activityTestRule);
+ addProcessObserver(mObserver);
+ mObserver.mChangedState = false;
+ return firstActivity;
+ }
+
+ private void addProcessObserver(LifecycleObserver observer) throws Throwable {
+ activityTestRule.runOnUiThread(() ->
+ ProcessLifecycleOwner.get().getLifecycle().addObserver(observer));
+ }
+
+ private void removeProcessObserver(LifecycleObserver observer) throws Throwable {
+ activityTestRule.runOnUiThread(() ->
+ ProcessLifecycleOwner.get().getLifecycle().removeObserver(observer));
+ }
+
+ private void checkProcessObserverSilent(FragmentActivity activity) throws Throwable {
+ waitTillResumed(activity, activityTestRule);
+ assertThat(mObserver.mChangedState, is(false));
+ activityTestRule.runOnUiThread(() ->
+ ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver));
+ }
+
+ private void checkProcessObserverSilent(NonSupportActivity activity) throws Throwable {
+ assertThat(activity.awaitResumedState(), is(true));
+ assertThat(mObserver.mChangedState, is(false));
+ activityTestRule.runOnUiThread(() ->
+ ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver));
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/SimpleAppFullLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/SimpleAppFullLifecycleTest.java
new file mode 100644
index 0000000..3a79160
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/SimpleAppFullLifecycleTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.State.CREATED;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isIn;
+import static org.hamcrest.Matchers.notNullValue;
+
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.testapp.SimpleAppLifecycleTestActivity;
+import androidx.lifecycle.testapp.SimpleAppLifecycleTestActivity.TestEventType;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class SimpleAppFullLifecycleTest {
+
+ @SuppressWarnings("unchecked")
+ private static final Pair[] EXPECTED_EVENTS_CONSTRUCTION =
+ new Pair[] {
+ new Pair(TestEventType.PROCESS_EVENT, Event.ON_CREATE),
+ new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_CREATE),
+ new Pair(TestEventType.PROCESS_EVENT, Event.ON_START),
+ new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_START),
+ new Pair(TestEventType.PROCESS_EVENT, Event.ON_RESUME),
+ new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_RESUME),
+ };
+
+ @SuppressWarnings("unchecked")
+ private static final Pair[] EXPECTED_EVENTS_DESTRUCTION =
+ new Pair[]{
+
+ new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_PAUSE),
+ new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_STOP),
+ new Pair(TestEventType.ACTIVITY_EVENT, Event.ON_DESTROY),
+
+ new Pair(TestEventType.PROCESS_EVENT, Event.ON_PAUSE),
+ new Pair(TestEventType.PROCESS_EVENT, Event.ON_STOP),
+ };
+ @Rule
+ public ActivityTestRule<SimpleAppLifecycleTestActivity> activityTestRule =
+ new ActivityTestRule<>(SimpleAppLifecycleTestActivity.class, false, false);
+
+ @Before
+ public void setup() {
+ // cool down period, so application state will become DESTROYED
+ try {
+ Thread.sleep(ProcessLifecycleOwner.TIMEOUT_MS * 2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ SimpleAppLifecycleTestActivity.startProcessObserver();
+ }
+
+ @After
+ public void tearDown() {
+ SimpleAppLifecycleTestActivity.stopProcessObserver();
+ }
+
+ @Test
+ public void testFullLifecycle() throws InterruptedException {
+ State currentState = ProcessLifecycleOwner.get().getLifecycle().getCurrentState();
+ assertThat(currentState, is(CREATED));
+ activityTestRule.launchActivity(null);
+ List<Pair<TestEventType, Event>> events = SimpleAppLifecycleTestActivity.awaitForEvents();
+ assertThat("Failed to await for events", events, notNullValue());
+ //noinspection ConstantConditions
+ assertThat(events.subList(0, 6).toArray(), is(EXPECTED_EVENTS_CONSTRUCTION));
+
+ // TODO: bug 35122523
+ for (Pair<TestEventType, Event> event: events.subList(6, 11)) {
+ assertThat(event, isIn(EXPECTED_EVENTS_DESTRUCTION));
+ }
+ }
+
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/SynchronousActivityLifecycleTest.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/SynchronousActivityLifecycleTest.java
new file mode 100644
index 0000000..7c06e62
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/SynchronousActivityLifecycleTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Build;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.UiThreadTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.testapp.LifecycleTestActivity;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+
+/**
+ * It tests that an event is dispatched immediately after a call of corresponding OnXXX method
+ * during an execution of performXXX
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SynchronousActivityLifecycleTest {
+
+ @Rule
+ public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();
+
+ @Test
+ public void testOnCreateCall() throws Throwable {
+ testSynchronousCall(Event.ON_CREATE,
+ activity -> {
+ },
+ activity -> getInstrumentation().callActivityOnCreate(activity, null));
+ }
+
+ @Test
+ public void testOnStartCall() throws Throwable {
+ testSynchronousCall(Lifecycle.Event.ON_START,
+ activity -> getInstrumentation().callActivityOnCreate(activity, null),
+ SynchronousActivityLifecycleTest::performStart);
+ }
+
+ @Test
+ public void testOnResumeCall() throws Throwable {
+ testSynchronousCall(Lifecycle.Event.ON_RESUME,
+ activity -> {
+ getInstrumentation().callActivityOnCreate(activity, null);
+ performStart(activity);
+ },
+ SynchronousActivityLifecycleTest::performResume);
+ }
+
+ @Test
+ public void testOnStopCall() throws Throwable {
+ testSynchronousCall(Lifecycle.Event.ON_STOP,
+ activity -> {
+ getInstrumentation().callActivityOnCreate(activity, null);
+ performStart(activity);
+ },
+ SynchronousActivityLifecycleTest::performStop);
+ }
+
+ @Test
+ public void testOnDestroyCall() throws Throwable {
+ testSynchronousCall(Lifecycle.Event.ON_DESTROY,
+ activity -> getInstrumentation().callActivityOnCreate(activity, null),
+ activity -> getInstrumentation().callActivityOnDestroy(activity));
+ }
+
+ public void testSynchronousCall(Event event, ActivityCall preInit, ActivityCall call)
+ throws Throwable {
+ uiThreadTestRule.runOnUiThread(() -> {
+ Intent intent = new Intent();
+ ComponentName cn = new ComponentName(LifecycleTestActivity.class.getPackage().getName(),
+ LifecycleTestActivity.class.getName());
+ intent.setComponent(cn);
+ Instrumentation instrumentation = getInstrumentation();
+ try {
+ Application app =
+ (Application) instrumentation.getTargetContext().getApplicationContext();
+ LifecycleTestActivity testActivity =
+ (LifecycleTestActivity) instrumentation.newActivity(
+ LifecycleTestActivity.class, instrumentation.getTargetContext(),
+ null, app, intent, new ActivityInfo(), "bla", null, null, null);
+ preInit.call(testActivity);
+ TestObserver testObserver = new TestObserver(testActivity, event);
+ testActivity.getLifecycle().addObserver(testObserver);
+ testObserver.unmute();
+ call.call(testActivity);
+
+ assertThat(testObserver.mEventReceived, is(true));
+ } catch (Exception e) {
+ throw new Error(e);
+ }
+ });
+ }
+
+ // Instrumentation.callOnActivityCreate calls performCreate on mActivity,
+ // but Instrumentation.callOnActivityStart calls onStart instead of performStart. ¯\_(ツ)_/¯
+ private static void performStart(Activity activity) {
+ try {
+ Method m = Activity.class.getDeclaredMethod("performStart");
+ m.setAccessible(true);
+ m.invoke(activity);
+ } catch (Exception e) {
+ throw new Error(e);
+ }
+ }
+
+ private static void performResume(Activity activity) {
+ try {
+ Method m = Activity.class.getDeclaredMethod("performResume");
+ m.setAccessible(true);
+ m.invoke(activity);
+ } catch (Exception e) {
+ throw new Error(e);
+ }
+ }
+
+
+ private static void performStop(Activity activity) {
+ try {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Method m = Activity.class.getDeclaredMethod("performStop", boolean.class);
+ m.setAccessible(true);
+ m.invoke(activity, false);
+ } else {
+ Method m = Activity.class.getDeclaredMethod("performStop");
+ m.setAccessible(true);
+ m.invoke(activity);
+ }
+ } catch (Exception e) {
+ throw new Error(e);
+ }
+ }
+
+ private static class TestObserver implements GenericLifecycleObserver {
+ private final LifecycleTestActivity mActivity;
+ private final Event mExpectedEvent;
+ boolean mEventReceived = false;
+ boolean mMuted = true;
+
+ private TestObserver(LifecycleTestActivity activity, Event expectedEvent) {
+ this.mActivity = activity;
+ this.mExpectedEvent = expectedEvent;
+ }
+
+ void unmute() {
+ mMuted = false;
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner lifecycleOwner, Event event) {
+ if (mMuted) {
+ return;
+ }
+ assertThat(event, is(mExpectedEvent));
+ assertThat(mActivity.mLifecycleCallFinished, is(true));
+ mEventReceived = true;
+ }
+ }
+
+ private interface ActivityCall {
+ void call(Activity activity);
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/TestUtils.java b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/TestUtils.java
new file mode 100644
index 0000000..2045181
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/androidTest/java/androidx/lifecycle/TestUtils.java
@@ -0,0 +1,168 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
+import static androidx.lifecycle.Lifecycle.State.DESTROYED;
+import static androidx.lifecycle.Lifecycle.State.RESUMED;
+import static androidx.lifecycle.Lifecycle.State.STARTED;
+import static androidx.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT;
+import static androidx.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.Instrumentation.ActivityMonitor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.testapp.TestEvent;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class TestUtils {
+
+ private static final long TIMEOUT_MS = 2000;
+
+ @SuppressWarnings("unchecked")
+ static <T extends Activity> T recreateActivity(final T activity, ActivityTestRule rule)
+ throws Throwable {
+ ActivityMonitor monitor = new ActivityMonitor(
+ activity.getClass().getCanonicalName(), null, false);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.addMonitor(monitor);
+ rule.runOnUiThread(activity::recreate);
+ T result;
+
+ // this guarantee that we will reinstall monitor between notifications about onDestroy
+ // and onCreate
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (monitor) {
+ do {
+ // the documetation says "Block until an Activity is created
+ // that matches this monitor." This statement is true, but there are some other
+ // true statements like: "Block until an Activity is destoyed" or
+ // "Block until an Activity is resumed"...
+
+ // this call will release synchronization monitor's monitor
+ result = (T) monitor.waitForActivityWithTimeout(TIMEOUT_MS);
+ if (result == null) {
+ throw new RuntimeException("Timeout. Failed to recreate an activity");
+ }
+ } while (result == activity);
+ }
+ return result;
+ }
+
+ static void waitTillCreated(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
+ throws Throwable {
+ waitTillState(owner, activityRule, CREATED);
+ }
+
+ static void waitTillStarted(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
+ throws Throwable {
+ waitTillState(owner, activityRule, STARTED);
+ }
+
+ static void waitTillResumed(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
+ throws Throwable {
+ waitTillState(owner, activityRule, RESUMED);
+ }
+
+ static void waitTillDestroyed(final LifecycleOwner owner, ActivityTestRule<?> activityRule)
+ throws Throwable {
+ waitTillState(owner, activityRule, DESTROYED);
+ }
+
+ static void waitTillState(final LifecycleOwner owner, ActivityTestRule<?> activityRule,
+ Lifecycle.State state)
+ throws Throwable {
+ final CountDownLatch latch = new CountDownLatch(1);
+ activityRule.runOnUiThread(() -> {
+ Lifecycle.State currentState = owner.getLifecycle().getCurrentState();
+ if (currentState == state) {
+ latch.countDown();
+ } else {
+ owner.getLifecycle().addObserver(new LifecycleObserver() {
+ @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
+ public void onStateChanged(LifecycleOwner provider) {
+ if (provider.getLifecycle().getCurrentState() == state) {
+ latch.countDown();
+ provider.getLifecycle().removeObserver(this);
+ }
+ }
+ });
+ }
+ });
+ boolean latchResult = latch.await(1, TimeUnit.MINUTES);
+ assertThat("expected " + state + " never happened. Current state:"
+ + owner.getLifecycle().getCurrentState(), latchResult, is(true));
+
+ // wait for another loop to ensure all observers are called
+ activityRule.runOnUiThread(() -> {
+ // do nothing
+ });
+ }
+
+ @SafeVarargs
+ static <T> List<T> flatMap(List<T>... items) {
+ ArrayList<T> result = new ArrayList<>();
+ for (List<T> item : items) {
+ result.addAll(item);
+ }
+ return result;
+ }
+
+ /**
+ * Event tuples of {@link TestEvent} and {@link Lifecycle.Event}
+ * in the order they should arrive.
+ */
+ @SuppressWarnings("unchecked")
+ static class OrderedTuples {
+ static final List<Pair<TestEvent, Lifecycle.Event>> CREATE =
+ Arrays.asList(new Pair(OWNER_CALLBACK, ON_CREATE),
+ new Pair(LIFECYCLE_EVENT, ON_CREATE));
+ static final List<Pair<TestEvent, Lifecycle.Event>> START =
+ Arrays.asList(new Pair(OWNER_CALLBACK, ON_START),
+ new Pair(LIFECYCLE_EVENT, ON_START));
+ static final List<Pair<TestEvent, Lifecycle.Event>> RESUME =
+ Arrays.asList(new Pair(OWNER_CALLBACK, ON_RESUME),
+ new Pair(LIFECYCLE_EVENT, ON_RESUME));
+ static final List<Pair<TestEvent, Lifecycle.Event>> PAUSE =
+ Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_PAUSE),
+ new Pair(OWNER_CALLBACK, ON_PAUSE));
+ static final List<Pair<TestEvent, Lifecycle.Event>> STOP =
+ Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_STOP),
+ new Pair(OWNER_CALLBACK, ON_STOP));
+ static final List<Pair<TestEvent, Lifecycle.Event>> DESTROY =
+ Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_DESTROY),
+ new Pair(OWNER_CALLBACK, ON_DESTROY));
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/AndroidManifest.xml b/lifecycle/integration-tests/testapp/src/main/AndroidManifest.xml
index 5e1d0a0..19166dd 100644
--- a/lifecycle/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/lifecycle/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -11,37 +11,37 @@
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools" package="android.arch.lifecycle.testapp">
+ xmlns:tools="http://schemas.android.com/tools" package="androidx.lifecycle.testapp">
<application android:allowBackup="true" android:label="Test App" android:supportsRtl="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon">
- <activity android:name="android.arch.lifecycle.testapp.LifecycleTestActivity">
+ <activity android:name="androidx.lifecycle.testapp.LifecycleTestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <activity android:name="android.arch.lifecycle.testapp.CollectingSupportActivity">
+ <activity android:name="androidx.lifecycle.testapp.CollectingSupportActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <activity android:name="android.arch.lifecycle.testapp.FrameworkLifecycleRegistryActivity">
+ <activity android:name="androidx.lifecycle.testapp.FrameworkLifecycleRegistryActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
- android:name="android.arch.lifecycle.testapp.SimpleAppLifecycleTestActivity">
+ android:name="androidx.lifecycle.testapp.SimpleAppLifecycleTestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <activity android:name="android.arch.lifecycle.testapp.NavigationTestActivityFirst"
+ <activity android:name="androidx.lifecycle.testapp.NavigationTestActivityFirst"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -49,7 +49,7 @@
</intent-filter>
</activity>
<activity
- android:name="android.arch.lifecycle.testapp.EmptyActivity">
+ android:name="androidx.lifecycle.testapp.EmptyActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java
deleted file mode 100644
index 4213cab..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.testapp;
-
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.support.v4.util.Pair;
-
-import java.util.List;
-
-/**
- * For activities that collect their events.
- */
-public interface CollectingLifecycleOwner extends LifecycleOwner {
- /**
- * Return a copy of currently collected events
- *
- * @return The list of collected events.
- * @throws InterruptedException
- */
- List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents();
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingSupportActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingSupportActivity.java
deleted file mode 100644
index f38d422..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingSupportActivity.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.testapp;
-
-import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
-
-import android.arch.lifecycle.Lifecycle.Event;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.util.Pair;
-import android.widget.FrameLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * LifecycleRegistryOwner that extends FragmentActivity.
- */
-public class CollectingSupportActivity extends FragmentActivity implements
- CollectingLifecycleOwner {
-
- private final List<Pair<TestEvent, Event>> mCollectedEvents = new ArrayList<>();
- private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
- private CountDownLatch mSavedStateLatch = new CountDownLatch(1);
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- FrameLayout layout = new FrameLayout(this);
- layout.setId(R.id.fragment_container);
- setContentView(layout);
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_CREATE));
- getLifecycle().addObserver(mTestObserver);
- }
-
- /**
- * replaces the main content fragment w/ the given fragment.
- */
- public void replaceFragment(Fragment fragment) {
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.fragment_container, fragment)
- .commitNow();
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_START));
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_RESUME));
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_DESTROY));
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_STOP));
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_PAUSE));
- // helps with less flaky API 16 tests.
- overridePendingTransition(0, 0);
- }
-
- @Override
- public List<Pair<TestEvent, Event>> copyCollectedEvents() {
- return new ArrayList<>(mCollectedEvents);
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mSavedStateLatch.countDown();
- }
-
- /**
- * Waits for onSaveInstanceState to be called.
- */
- public boolean waitForStateSave(@SuppressWarnings("SameParameterValue") int seconds)
- throws InterruptedException {
- return mSavedStateLatch.await(seconds, TimeUnit.SECONDS);
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingSupportFragment.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingSupportFragment.java
deleted file mode 100644
index 9bbbe16..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/CollectingSupportFragment.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.testapp;
-
-import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
-
-import android.annotation.SuppressLint;
-import android.arch.lifecycle.Lifecycle;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
-import android.support.v4.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A support fragment that collects all of its events.
- */
-@SuppressLint("ValidFragment")
-public class CollectingSupportFragment extends Fragment implements CollectingLifecycleOwner {
- private final List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents =
- new ArrayList<>();
- private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE));
- getLifecycle().addObserver(mTestObserver);
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- //noinspection ConstantConditions
- FrameLayout layout = new FrameLayout(container.getContext());
- layout.setId(R.id.child_fragment_container);
- return layout;
- }
-
- /**
- * Runs a replace fragment transaction with 'fragment' on this Fragment.
- */
- public void replaceFragment(Fragment fragment) {
- getChildFragmentManager()
- .beginTransaction()
- .add(R.id.child_fragment_container, fragment)
- .commitNow();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START));
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME));
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY));
- }
-
- @Override
- public void onStop() {
- super.onStop();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP));
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE));
- }
-
- @Override
- public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() {
- return new ArrayList<>(mCollectedEvents);
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/EmptyActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/EmptyActivity.java
deleted file mode 100644
index 68a82e1..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/EmptyActivity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.testapp;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.app.FragmentActivity;
-
-/**
- * empty activity
- */
-public class EmptyActivity extends FragmentActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.empty_activity_layout);
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java
deleted file mode 100644
index cdf577c..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.testapp;
-
-import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
-
-import android.app.Activity;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleRegistry;
-import android.arch.lifecycle.LifecycleRegistryOwner;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.util.Pair;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-
-/**
- * LifecycleRegistryOwner that extends framework activity.
- */
-@SuppressWarnings("deprecation")
-public class FrameworkLifecycleRegistryActivity extends Activity implements
- LifecycleRegistryOwner, CollectingLifecycleOwner {
- private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
-
- @NonNull
- @Override
- public LifecycleRegistry getLifecycle() {
- return mLifecycleRegistry;
- }
-
- private List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = new ArrayList<>();
- private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
- private CountDownLatch mLatch = new CountDownLatch(1);
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE));
- getLifecycle().addObserver(mTestObserver);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START));
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME));
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY));
- mLatch.countDown();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP));
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE));
- }
-
- @Override
- public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() {
- return new ArrayList<>(mCollectedEvents);
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/LifecycleTestActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/LifecycleTestActivity.java
deleted file mode 100644
index cf07aee..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/LifecycleTestActivity.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2016 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.testapp;
-
-import android.os.Bundle;
-import android.support.v4.app.FragmentActivity;
-
-/**
- * Activity for testing events by themselves
- */
-public class LifecycleTestActivity extends FragmentActivity {
-
- /**
- * identifies that
- */
- public boolean mLifecycleCallFinished;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- mLifecycleCallFinished = false;
- super.onCreate(savedInstanceState);
- mLifecycleCallFinished = true;
- }
-
- @Override
- protected void onStart() {
- mLifecycleCallFinished = false;
- super.onStart();
- mLifecycleCallFinished = true;
- }
-
- @Override
- protected void onResume() {
- mLifecycleCallFinished = false;
- super.onResume();
- mLifecycleCallFinished = true;
- }
-
- @Override
- protected void onPause() {
- mLifecycleCallFinished = false;
- super.onPause();
- mLifecycleCallFinished = true;
- }
-
- @Override
- protected void onStop() {
- mLifecycleCallFinished = false;
- super.onStop();
- mLifecycleCallFinished = true;
- }
-
- @Override
- protected void onDestroy() {
- mLifecycleCallFinished = false;
- super.onDestroy();
- mLifecycleCallFinished = true;
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationDialogActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationDialogActivity.java
deleted file mode 100644
index 7d53528..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationDialogActivity.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.testapp;
-
-import android.support.v4.app.FragmentActivity;
-
-/**
- * an activity with Dialog theme.
- */
-public class NavigationDialogActivity extends FragmentActivity {
- @Override
- protected void onPause() {
- super.onPause();
- // helps with less flaky API 16 tests
- overridePendingTransition(0, 0);
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java
deleted file mode 100644
index 69fd478..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.testapp;
-
-import android.support.v4.app.FragmentActivity;
-
-/**
- * Activity for ProcessOwnerTest
- */
-public class NavigationTestActivityFirst extends FragmentActivity {
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java
deleted file mode 100644
index 0f9a4c9..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.testapp;
-
-import android.support.v4.app.FragmentActivity;
-
-/**
- * Activity for ProcessOwnerTest
- */
-public class NavigationTestActivitySecond extends FragmentActivity {
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NonSupportActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NonSupportActivity.java
deleted file mode 100644
index 835d846..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/NonSupportActivity.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.testapp;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Activity which doesn't extend FragmentActivity, to test ProcessLifecycleOwner because it
- * should work anyway.
- */
-public class NonSupportActivity extends Activity {
-
- private static final int TIMEOUT = 1; //secs
- private final Lock mLock = new ReentrantLock();
- private Condition mIsResumedCondition = mLock.newCondition();
- private boolean mIsResumed = false;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- mLock.lock();
- try {
- mIsResumed = true;
- mIsResumedCondition.signalAll();
- } finally {
- mLock.unlock();
- }
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mLock.lock();
- try {
- mIsResumed = false;
- } finally {
- mLock.unlock();
- }
- }
-
- /**
- * awaits resumed state
- * @return
- * @throws InterruptedException
- */
- public boolean awaitResumedState() throws InterruptedException {
- mLock.lock();
- try {
- while (!mIsResumed) {
- if (!mIsResumedCondition.await(TIMEOUT, TimeUnit.SECONDS)) {
- return false;
- }
- }
- return true;
- } finally {
- mLock.unlock();
- }
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
deleted file mode 100644
index 77bd99f..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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.testapp;
-
-import android.arch.lifecycle.Lifecycle;
-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;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Activity for SimpleAppFullLifecycleTest
- */
-public class SimpleAppLifecycleTestActivity extends FragmentActivity {
-
- public enum TestEventType {
- PROCESS_EVENT,
- ACTIVITY_EVENT
- }
-
- private static final long TIMEOUT_SECS = 10; // secs
-
- static class TestObserver implements LifecycleObserver {
-
- private TestEventType mType;
-
- TestObserver(TestEventType type) {
- mType = type;
- }
-
- @SuppressWarnings("unused")
- @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
- void onEvent(LifecycleOwner provider, Lifecycle.Event event) {
- sCollectedEvents.add(new Pair<>(mType, event));
- sLatch.countDown();
- }
- }
-
- static List<Pair<TestEventType, Lifecycle.Event>> sCollectedEvents = new ArrayList<>();
- static CountDownLatch sLatch = new CountDownLatch(11);
-
- /**
- * start process observer
- */
- public static void startProcessObserver() {
- ProcessLifecycleOwner.get().getLifecycle().addObserver(sProcessObserver);
- }
-
- /**
- * stop process observer
- */
- public static void stopProcessObserver() {
- ProcessLifecycleOwner.get().getLifecycle().removeObserver(sProcessObserver);
- }
-
- private static TestObserver sProcessObserver = new TestObserver(TestEventType.PROCESS_EVENT);
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- getLifecycle().addObserver(new TestObserver(TestEventType.ACTIVITY_EVENT));
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- finish();
- }
-
- /**
- * returns collected events
- */
- public static List<Pair<TestEventType, Lifecycle.Event>> awaitForEvents()
- throws InterruptedException {
- boolean success = sLatch.await(TIMEOUT_SECS, TimeUnit.SECONDS);
- return success ? sCollectedEvents : null;
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/TestEvent.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/TestEvent.java
deleted file mode 100644
index 788045a..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/TestEvent.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.testapp;
-
-public enum TestEvent {
- OWNER_CALLBACK,
- LIFECYCLE_EVENT,
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/TestObserver.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/TestObserver.java
deleted file mode 100644
index 00b8e16..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/TestObserver.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.testapp;
-
-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.testapp.TestEvent.LIFECYCLE_EVENT;
-
-import android.arch.lifecycle.Lifecycle.Event;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.OnLifecycleEvent;
-import android.support.v4.util.Pair;
-
-import java.util.List;
-
-class TestObserver implements LifecycleObserver {
- private final List<Pair<TestEvent, Event>> mCollectedEvents;
-
- TestObserver(List<Pair<TestEvent, Event>> collectedEvents) {
- mCollectedEvents = collectedEvents;
- }
-
- @OnLifecycleEvent(ON_CREATE)
- public void create(LifecycleOwner pr) {
- mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_CREATE));
- }
-
- @OnLifecycleEvent(ON_START)
- public void start(LifecycleOwner pr) {
- mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_START));
- }
-
- @OnLifecycleEvent(ON_RESUME)
- public void resume(LifecycleOwner pr) {
- mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_RESUME));
- }
- @OnLifecycleEvent(ON_PAUSE)
- public void pause(LifecycleOwner pr) {
- mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_PAUSE));
- }
-
- @OnLifecycleEvent(ON_STOP)
- public void stop(LifecycleOwner pr) {
- mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_STOP));
- }
-
- @OnLifecycleEvent(ON_DESTROY)
- public void destroy(LifecycleOwner pr) {
- mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_DESTROY));
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/UsualFragment.java b/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/UsualFragment.java
deleted file mode 100644
index fb6cae0..0000000
--- a/lifecycle/integration-tests/testapp/src/main/java/android/arch/lifecycle/testapp/UsualFragment.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 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.testapp;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Simple fragment which does nothing.
- */
-public class UsualFragment extends Fragment {
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return new View(getContext());
- }
-}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingLifecycleOwner.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingLifecycleOwner.java
new file mode 100644
index 0000000..95353cb
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingLifecycleOwner.java
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.List;
+
+/**
+ * For activities that collect their events.
+ */
+public interface CollectingLifecycleOwner extends LifecycleOwner {
+ /**
+ * Return a copy of currently collected events
+ *
+ * @return The list of collected events.
+ * @throws InterruptedException
+ */
+ List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents();
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingSupportActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingSupportActivity.java
new file mode 100644
index 0000000..65cf61f
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingSupportActivity.java
@@ -0,0 +1,114 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import static androidx.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
+
+import android.os.Bundle;
+import android.widget.FrameLayout;
+
+import androidx.core.util.Pair;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Lifecycle.Event;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * LifecycleRegistryOwner that extends FragmentActivity.
+ */
+public class CollectingSupportActivity extends FragmentActivity implements
+ CollectingLifecycleOwner {
+
+ private final List<Pair<TestEvent, Event>> mCollectedEvents = new ArrayList<>();
+ private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
+ private CountDownLatch mSavedStateLatch = new CountDownLatch(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ FrameLayout layout = new FrameLayout(this);
+ layout.setId(R.id.fragment_container);
+ setContentView(layout);
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_CREATE));
+ getLifecycle().addObserver(mTestObserver);
+ }
+
+ /**
+ * replaces the main content fragment w/ the given fragment.
+ */
+ public void replaceFragment(Fragment fragment) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(R.id.fragment_container, fragment)
+ .commitNow();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_START));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_RESUME));
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_DESTROY));
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_STOP));
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_PAUSE));
+ // helps with less flaky API 16 tests.
+ overridePendingTransition(0, 0);
+ }
+
+ @Override
+ public List<Pair<TestEvent, Event>> copyCollectedEvents() {
+ return new ArrayList<>(mCollectedEvents);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mSavedStateLatch.countDown();
+ }
+
+ /**
+ * Waits for onSaveInstanceState to be called.
+ */
+ public boolean waitForStateSave(@SuppressWarnings("SameParameterValue") int seconds)
+ throws InterruptedException {
+ return mSavedStateLatch.await(seconds, TimeUnit.SECONDS);
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingSupportFragment.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingSupportFragment.java
new file mode 100644
index 0000000..5add2f4
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/CollectingSupportFragment.java
@@ -0,0 +1,105 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import static androidx.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A support fragment that collects all of its events.
+ */
+@SuppressLint("ValidFragment")
+public class CollectingSupportFragment extends Fragment implements CollectingLifecycleOwner {
+ private final List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents =
+ new ArrayList<>();
+ private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE));
+ getLifecycle().addObserver(mTestObserver);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ //noinspection ConstantConditions
+ FrameLayout layout = new FrameLayout(container.getContext());
+ layout.setId(R.id.child_fragment_container);
+ return layout;
+ }
+
+ /**
+ * Runs a replace fragment transaction with 'fragment' on this Fragment.
+ */
+ public void replaceFragment(Fragment fragment) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .add(R.id.child_fragment_container, fragment)
+ .commitNow();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY));
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP));
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE));
+ }
+
+ @Override
+ public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() {
+ return new ArrayList<>(mCollectedEvents);
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/EmptyActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/EmptyActivity.java
new file mode 100644
index 0000000..2cb4c96
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/EmptyActivity.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 androidx.lifecycle.testapp;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * empty activity
+ */
+public class EmptyActivity extends FragmentActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.empty_activity_layout);
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java
new file mode 100644
index 0000000..d6ffbfb
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java
@@ -0,0 +1,94 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import static androidx.lifecycle.testapp.TestEvent.OWNER_CALLBACK;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Pair;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LifecycleRegistryOwner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * LifecycleRegistryOwner that extends framework activity.
+ */
+@SuppressWarnings("deprecation")
+public class FrameworkLifecycleRegistryActivity extends Activity implements
+ LifecycleRegistryOwner, CollectingLifecycleOwner {
+ private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
+
+ @NonNull
+ @Override
+ public LifecycleRegistry getLifecycle() {
+ return mLifecycleRegistry;
+ }
+
+ private List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = new ArrayList<>();
+ private TestObserver mTestObserver = new TestObserver(mCollectedEvents);
+ private CountDownLatch mLatch = new CountDownLatch(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE));
+ getLifecycle().addObserver(mTestObserver);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME));
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY));
+ mLatch.countDown();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP));
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE));
+ }
+
+ @Override
+ public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() {
+ return new ArrayList<>(mCollectedEvents);
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/LifecycleTestActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/LifecycleTestActivity.java
new file mode 100644
index 0000000..f600c69
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/LifecycleTestActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.testapp;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Activity for testing events by themselves
+ */
+public class LifecycleTestActivity extends FragmentActivity {
+
+ /**
+ * identifies that
+ */
+ public boolean mLifecycleCallFinished;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ mLifecycleCallFinished = false;
+ super.onCreate(savedInstanceState);
+ mLifecycleCallFinished = true;
+ }
+
+ @Override
+ protected void onStart() {
+ mLifecycleCallFinished = false;
+ super.onStart();
+ mLifecycleCallFinished = true;
+ }
+
+ @Override
+ protected void onResume() {
+ mLifecycleCallFinished = false;
+ super.onResume();
+ mLifecycleCallFinished = true;
+ }
+
+ @Override
+ protected void onPause() {
+ mLifecycleCallFinished = false;
+ super.onPause();
+ mLifecycleCallFinished = true;
+ }
+
+ @Override
+ protected void onStop() {
+ mLifecycleCallFinished = false;
+ super.onStop();
+ mLifecycleCallFinished = true;
+ }
+
+ @Override
+ protected void onDestroy() {
+ mLifecycleCallFinished = false;
+ super.onDestroy();
+ mLifecycleCallFinished = true;
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationDialogActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationDialogActivity.java
new file mode 100644
index 0000000..81834b4
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationDialogActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * an activity with Dialog theme.
+ */
+public class NavigationDialogActivity extends FragmentActivity {
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // helps with less flaky API 16 tests
+ overridePendingTransition(0, 0);
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationTestActivityFirst.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationTestActivityFirst.java
new file mode 100644
index 0000000..28c1e7f
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationTestActivityFirst.java
@@ -0,0 +1,25 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Activity for ProcessOwnerTest
+ */
+public class NavigationTestActivityFirst extends FragmentActivity {
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationTestActivitySecond.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationTestActivitySecond.java
new file mode 100644
index 0000000..e78ec17
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NavigationTestActivitySecond.java
@@ -0,0 +1,25 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Activity for ProcessOwnerTest
+ */
+public class NavigationTestActivitySecond extends FragmentActivity {
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NonSupportActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NonSupportActivity.java
new file mode 100644
index 0000000..8b3ae72
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/NonSupportActivity.java
@@ -0,0 +1,86 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Activity which doesn't extend FragmentActivity, to test ProcessLifecycleOwner because it
+ * should work anyway.
+ */
+public class NonSupportActivity extends Activity {
+
+ private static final int TIMEOUT = 1; //secs
+ private final Lock mLock = new ReentrantLock();
+ private Condition mIsResumedCondition = mLock.newCondition();
+ private boolean mIsResumed = false;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mLock.lock();
+ try {
+ mIsResumed = true;
+ mIsResumedCondition.signalAll();
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mLock.lock();
+ try {
+ mIsResumed = false;
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * awaits resumed state
+ * @return
+ * @throws InterruptedException
+ */
+ public boolean awaitResumedState() throws InterruptedException {
+ mLock.lock();
+ try {
+ while (!mIsResumed) {
+ if (!mIsResumedCondition.await(TIMEOUT, TimeUnit.SECONDS)) {
+ return false;
+ }
+ }
+ return true;
+ } finally {
+ mLock.unlock();
+ }
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/SimpleAppLifecycleTestActivity.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
new file mode 100644
index 0000000..c739568
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/SimpleAppLifecycleTestActivity.java
@@ -0,0 +1,101 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+import android.os.Bundle;
+import android.util.Pair;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.ProcessLifecycleOwner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity for SimpleAppFullLifecycleTest
+ */
+public class SimpleAppLifecycleTestActivity extends FragmentActivity {
+
+ public enum TestEventType {
+ PROCESS_EVENT,
+ ACTIVITY_EVENT
+ }
+
+ private static final long TIMEOUT_SECS = 10; // secs
+
+ static class TestObserver implements LifecycleObserver {
+
+ private TestEventType mType;
+
+ TestObserver(TestEventType type) {
+ mType = type;
+ }
+
+ @SuppressWarnings("unused")
+ @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
+ void onEvent(LifecycleOwner provider, Lifecycle.Event event) {
+ sCollectedEvents.add(new Pair<>(mType, event));
+ sLatch.countDown();
+ }
+ }
+
+ static List<Pair<TestEventType, Lifecycle.Event>> sCollectedEvents = new ArrayList<>();
+ static CountDownLatch sLatch = new CountDownLatch(11);
+
+ /**
+ * start process observer
+ */
+ public static void startProcessObserver() {
+ ProcessLifecycleOwner.get().getLifecycle().addObserver(sProcessObserver);
+ }
+
+ /**
+ * stop process observer
+ */
+ public static void stopProcessObserver() {
+ ProcessLifecycleOwner.get().getLifecycle().removeObserver(sProcessObserver);
+ }
+
+ private static TestObserver sProcessObserver = new TestObserver(TestEventType.PROCESS_EVENT);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getLifecycle().addObserver(new TestObserver(TestEventType.ACTIVITY_EVENT));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ finish();
+ }
+
+ /**
+ * returns collected events
+ */
+ public static List<Pair<TestEventType, Lifecycle.Event>> awaitForEvents()
+ throws InterruptedException {
+ boolean success = sLatch.await(TIMEOUT_SECS, TimeUnit.SECONDS);
+ return success ? sCollectedEvents : null;
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/TestEvent.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/TestEvent.java
new file mode 100644
index 0000000..18ed17a
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/TestEvent.java
@@ -0,0 +1,22 @@
+/*
+ * 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 androidx.lifecycle.testapp;
+
+public enum TestEvent {
+ OWNER_CALLBACK,
+ LIFECYCLE_EVENT,
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/TestObserver.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/TestObserver.java
new file mode 100644
index 0000000..7a3b249
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/TestObserver.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 androidx.lifecycle.testapp;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+
+import java.util.List;
+
+class TestObserver implements LifecycleObserver {
+ private final List<Pair<TestEvent, Event>> mCollectedEvents;
+
+ TestObserver(List<Pair<TestEvent, Event>> collectedEvents) {
+ mCollectedEvents = collectedEvents;
+ }
+
+ @OnLifecycleEvent(ON_CREATE)
+ public void create(LifecycleOwner pr) {
+ mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_CREATE));
+ }
+
+ @OnLifecycleEvent(ON_START)
+ public void start(LifecycleOwner pr) {
+ mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_START));
+ }
+
+ @OnLifecycleEvent(ON_RESUME)
+ public void resume(LifecycleOwner pr) {
+ mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_RESUME));
+ }
+ @OnLifecycleEvent(ON_PAUSE)
+ public void pause(LifecycleOwner pr) {
+ mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_PAUSE));
+ }
+
+ @OnLifecycleEvent(ON_STOP)
+ public void stop(LifecycleOwner pr) {
+ mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_STOP));
+ }
+
+ @OnLifecycleEvent(ON_DESTROY)
+ public void destroy(LifecycleOwner pr) {
+ mCollectedEvents.add(new Pair<>(LIFECYCLE_EVENT, ON_DESTROY));
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/UsualFragment.java b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/UsualFragment.java
new file mode 100644
index 0000000..242b3c2
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/main/java/androidx/lifecycle/testapp/UsualFragment.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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 androidx.lifecycle.testapp;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+/**
+ * Simple fragment which does nothing.
+ */
+public class UsualFragment extends Fragment {
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return new View(getContext());
+ }
+}
diff --git a/lifecycle/integration-tests/testapp/src/main/res/layout/empty_activity_layout.xml b/lifecycle/integration-tests/testapp/src/main/res/layout/empty_activity_layout.xml
index d476848..d906eb8 100644
--- a/lifecycle/integration-tests/testapp/src/main/res/layout/empty_activity_layout.xml
+++ b/lifecycle/integration-tests/testapp/src/main/res/layout/empty_activity_layout.xml
@@ -21,7 +21,7 @@
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context="android.arch.lifecycle.activity.FragmentLifecycleActivity">
+ tools:context="androidx.lifecycle.activity.FragmentLifecycleActivity">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
diff --git a/lifecycle/integration-tests/testapp/src/test/java/android/arch/lifecycle/GeneratedAdaptersTest.java b/lifecycle/integration-tests/testapp/src/test/java/android/arch/lifecycle/GeneratedAdaptersTest.java
deleted file mode 100644
index 2abb511..0000000
--- a/lifecycle/integration-tests/testapp/src/test/java/android/arch/lifecycle/GeneratedAdaptersTest.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * 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/lifecycle/integration-tests/testapp/src/test/java/androidx/lifecycle/GeneratedAdaptersTest.java b/lifecycle/integration-tests/testapp/src/test/java/androidx/lifecycle/GeneratedAdaptersTest.java
new file mode 100644
index 0000000..9ebb880
--- /dev/null
+++ b/lifecycle/integration-tests/testapp/src/test/java/androidx/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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
+import static androidx.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/lifecycle/livedata-core/api/current.txt b/lifecycle/livedata-core/api/current.txt
index 7e1d451..5cd4a08 100644
--- a/lifecycle/livedata-core/api/current.txt
+++ b/lifecycle/livedata-core/api/current.txt
@@ -1,21 +1,21 @@
-package android.arch.lifecycle {
+package androidx.lifecycle {
public abstract class LiveData<T> {
ctor public LiveData();
method public T getValue();
method public boolean hasActiveObservers();
method public boolean hasObservers();
- method public void observe(android.arch.lifecycle.LifecycleOwner, android.arch.lifecycle.Observer<? super T>);
- method public void observeForever(android.arch.lifecycle.Observer<? super T>);
+ method public void observe(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Observer<? super T>);
+ method public void observeForever(androidx.lifecycle.Observer<? super T>);
method protected void onActive();
method protected void onInactive();
method protected void postValue(T);
- method public void removeObserver(android.arch.lifecycle.Observer<? super T>);
- method public void removeObservers(android.arch.lifecycle.LifecycleOwner);
+ method public void removeObserver(androidx.lifecycle.Observer<? super T>);
+ method public void removeObservers(androidx.lifecycle.LifecycleOwner);
method protected void setValue(T);
}
- public class MutableLiveData<T> extends android.arch.lifecycle.LiveData {
+ public class MutableLiveData<T> extends androidx.lifecycle.LiveData {
ctor public MutableLiveData();
method public void postValue(T);
method public void setValue(T);
diff --git a/lifecycle/livedata-core/api/1.1.0.txt b/lifecycle/livedata-core/api_legacy/1.1.0.txt
similarity index 100%
rename from lifecycle/livedata-core/api/1.1.0.txt
rename to lifecycle/livedata-core/api_legacy/1.1.0.txt
diff --git a/lifecycle/livedata-core/api_legacy/current.txt b/lifecycle/livedata-core/api_legacy/current.txt
new file mode 100644
index 0000000..7e1d451
--- /dev/null
+++ b/lifecycle/livedata-core/api_legacy/current.txt
@@ -0,0 +1,29 @@
+package android.arch.lifecycle {
+
+ public abstract class LiveData<T> {
+ ctor public LiveData();
+ method public T getValue();
+ method public boolean hasActiveObservers();
+ method public boolean hasObservers();
+ method public void observe(android.arch.lifecycle.LifecycleOwner, android.arch.lifecycle.Observer<? super T>);
+ method public void observeForever(android.arch.lifecycle.Observer<? super T>);
+ method protected void onActive();
+ method protected void onInactive();
+ method protected void postValue(T);
+ method public void removeObserver(android.arch.lifecycle.Observer<? super T>);
+ method public void removeObservers(android.arch.lifecycle.LifecycleOwner);
+ method protected void setValue(T);
+ }
+
+ public class MutableLiveData<T> extends android.arch.lifecycle.LiveData {
+ ctor public MutableLiveData();
+ method public void postValue(T);
+ method public void setValue(T);
+ }
+
+ public abstract interface Observer<T> {
+ method public abstract void onChanged(T);
+ }
+
+}
+
diff --git a/lifecycle/livedata-core/build.gradle b/lifecycle/livedata-core/build.gradle
index becb17e..76f6c83 100644
--- a/lifecycle/livedata-core/build.gradle
+++ b/lifecycle/livedata-core/build.gradle
@@ -24,11 +24,11 @@
}
dependencies {
- implementation(project(":arch:common"))
- implementation(project(":arch:runtime"))
- api(project(":lifecycle:common"))
+ implementation(project(":arch:core-common"))
+ implementation(project(":arch:core-runtime"))
+ api(project(":lifecycle:lifecycle-common"))
- testImplementation(project(":lifecycle:runtime"))
+ testImplementation(project(":lifecycle:lifecycle-runtime"))
testImplementation(project(":arch:core-testing"))
testImplementation(JUNIT)
testImplementation(MOCKITO_CORE)
diff --git a/lifecycle/livedata-core/src/main/AndroidManifest.xml b/lifecycle/livedata-core/src/main/AndroidManifest.xml
index 6d2f964..43abea7 100644
--- a/lifecycle/livedata-core/src/main/AndroidManifest.xml
+++ b/lifecycle/livedata-core/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.livedata.core">
+ package="androidx.lifecycle.livedata.core">
</manifest>
diff --git a/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/LiveData.java b/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/LiveData.java
deleted file mode 100644
index b49185a..0000000
--- a/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/LiveData.java
+++ /dev/null
@@ -1,441 +0,0 @@
-/*
- * 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.State.DESTROYED;
-import static android.arch.lifecycle.Lifecycle.State.STARTED;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.internal.SafeIterableMap;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-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 held by this instance
- * @see ViewModel
- */
-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 SafeIterableMap<Observer<? super T>, ObserverWrapper> 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(ObserverWrapper observer) {
- if (!observer.mActive) {
- 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 (!observer.shouldBeActive()) {
- observer.activeStateChanged(false);
- return;
- }
- if (observer.mLastVersion >= mVersion) {
- return;
- }
- observer.mLastVersion = mVersion;
- //noinspection unchecked
- observer.mObserver.onChanged((T) mData);
- }
-
- private void dispatchingValue(@Nullable ObserverWrapper initiator) {
- if (mDispatchingValue) {
- mDispatchInvalidated = true;
- return;
- }
- mDispatchingValue = true;
- do {
- mDispatchInvalidated = false;
- if (initiator != null) {
- considerNotify(initiator);
- initiator = null;
- } else {
- for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> 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(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
- assertMainThread("observe");
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- // ignore
- return;
- }
- LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
- ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && !existing.isAttachedTo(owner)) {
- throw new IllegalArgumentException("Cannot add the same observer"
- + " with different lifecycles");
- }
- if (existing != null) {
- return;
- }
- owner.getLifecycle().addObserver(wrapper);
- }
-
- /**
- * 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(@NonNull Observer<? super T> observer) {
- assertMainThread("observeForever");
- AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
- ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
- throw new IllegalArgumentException("Cannot add the same observer"
- + " with different lifecycles");
- }
- if (existing != null) {
- return;
- }
- wrapper.activeStateChanged(true);
- }
-
- /**
- * Removes the given observer from the observers list.
- *
- * @param observer The Observer to receive events.
- */
- @MainThread
- public void removeObserver(@NonNull final Observer<? super T> observer) {
- assertMainThread("removeObserver");
- ObserverWrapper removed = mObservers.remove(observer);
- if (removed == null) {
- return;
- }
- removed.detachObserver();
- 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.
- */
- @SuppressWarnings("WeakerAccess")
- @MainThread
- public void removeObservers(@NonNull final LifecycleOwner owner) {
- assertMainThread("removeObservers");
- for (Map.Entry<Observer<? super T>, ObserverWrapper> entry : mObservers) {
- if (entry.getValue().isAttachedTo(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;
- }
- ArchTaskExecutor.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
- */
- @SuppressWarnings("WeakerAccess")
- public boolean hasObservers() {
- return mObservers.size() > 0;
- }
-
- /**
- * Returns true if this LiveData has active observers.
- *
- * @return true if this LiveData has active observers
- */
- @SuppressWarnings("WeakerAccess")
- public boolean hasActiveObservers() {
- return mActiveCount > 0;
- }
-
- class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
- @NonNull final LifecycleOwner mOwner;
-
- LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
- super(observer);
- mOwner = owner;
- }
-
- @Override
- boolean shouldBeActive() {
- return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
- }
-
- @Override
- public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
- if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
- removeObserver(mObserver);
- return;
- }
- activeStateChanged(shouldBeActive());
- }
-
- @Override
- boolean isAttachedTo(LifecycleOwner owner) {
- return mOwner == owner;
- }
-
- @Override
- void detachObserver() {
- mOwner.getLifecycle().removeObserver(this);
- }
- }
-
- private abstract class ObserverWrapper {
- final Observer<? super T> mObserver;
- boolean mActive;
- int mLastVersion = START_VERSION;
-
- ObserverWrapper(Observer<? super T> observer) {
- mObserver = observer;
- }
-
- abstract boolean shouldBeActive();
-
- boolean isAttachedTo(LifecycleOwner owner) {
- return false;
- }
-
- void detachObserver() {
- }
-
- void activeStateChanged(boolean newActive) {
- if (newActive == mActive) {
- return;
- }
- // immediately set active state, so we'd never dispatch anything to inactive
- // owner
- mActive = newActive;
- boolean wasInactive = LiveData.this.mActiveCount == 0;
- LiveData.this.mActiveCount += mActive ? 1 : -1;
- if (wasInactive && mActive) {
- onActive();
- }
- if (LiveData.this.mActiveCount == 0 && !mActive) {
- onInactive();
- }
- if (mActive) {
- dispatchingValue(this);
- }
- }
- }
-
- private class AlwaysActiveObserver extends ObserverWrapper {
-
- AlwaysActiveObserver(Observer<? super T> observer) {
- super(observer);
- }
-
- @Override
- boolean shouldBeActive() {
- return true;
- }
- }
-
- private static void assertMainThread(String methodName) {
- if (!ArchTaskExecutor.getInstance().isMainThread()) {
- throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
- + " thread");
- }
- }
-}
diff --git a/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/MutableLiveData.java b/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/MutableLiveData.java
deleted file mode 100644
index ecd7752..0000000
--- a/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/MutableLiveData.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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;
-
-/**
- * {@link LiveData} which publicly exposes {@link #setValue(T)} and {@link #postValue(T)} method.
- *
- * @param <T> The type of data hold by this instance
- */
-@SuppressWarnings("WeakerAccess")
-public class MutableLiveData<T> extends LiveData<T> {
- @Override
- public void postValue(T value) {
- super.postValue(value);
- }
-
- @Override
- public void setValue(T value) {
- super.setValue(value);
- }
-}
diff --git a/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/Observer.java b/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/Observer.java
deleted file mode 100644
index 0e36775..0000000
--- a/lifecycle/livedata-core/src/main/java/android/arch/lifecycle/Observer.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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;
-
-/**
- * A simple callback that can receive from {@link LiveData}.
- *
- * @param <T> The type of the parameter
- *
- * @see LiveData LiveData - for a usage description.
- */
-public interface Observer<T> {
- /**
- * Called when the data is changed.
- * @param t The new data
- */
- void onChanged(@Nullable T t);
-}
diff --git a/lifecycle/livedata-core/src/main/java/androidx/lifecycle/LiveData.java b/lifecycle/livedata-core/src/main/java/androidx/lifecycle/LiveData.java
new file mode 100644
index 0000000..8c94a95
--- /dev/null
+++ b/lifecycle/livedata-core/src/main/java/androidx/lifecycle/LiveData.java
@@ -0,0 +1,441 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.State.DESTROYED;
+import static androidx.lifecycle.Lifecycle.State.STARTED;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.internal.SafeIterableMap;
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+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 held by this instance
+ * @see ViewModel
+ */
+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 SafeIterableMap<Observer<? super T>, ObserverWrapper> 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(ObserverWrapper observer) {
+ if (!observer.mActive) {
+ 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 (!observer.shouldBeActive()) {
+ observer.activeStateChanged(false);
+ return;
+ }
+ if (observer.mLastVersion >= mVersion) {
+ return;
+ }
+ observer.mLastVersion = mVersion;
+ //noinspection unchecked
+ observer.mObserver.onChanged((T) mData);
+ }
+
+ private void dispatchingValue(@Nullable ObserverWrapper initiator) {
+ if (mDispatchingValue) {
+ mDispatchInvalidated = true;
+ return;
+ }
+ mDispatchingValue = true;
+ do {
+ mDispatchInvalidated = false;
+ if (initiator != null) {
+ considerNotify(initiator);
+ initiator = null;
+ } else {
+ for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> 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(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
+ assertMainThread("observe");
+ if (owner.getLifecycle().getCurrentState() == DESTROYED) {
+ // ignore
+ return;
+ }
+ LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
+ ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
+ if (existing != null && !existing.isAttachedTo(owner)) {
+ throw new IllegalArgumentException("Cannot add the same observer"
+ + " with different lifecycles");
+ }
+ if (existing != null) {
+ return;
+ }
+ owner.getLifecycle().addObserver(wrapper);
+ }
+
+ /**
+ * 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(@NonNull Observer<? super T> observer) {
+ assertMainThread("observeForever");
+ AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
+ ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
+ if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
+ throw new IllegalArgumentException("Cannot add the same observer"
+ + " with different lifecycles");
+ }
+ if (existing != null) {
+ return;
+ }
+ wrapper.activeStateChanged(true);
+ }
+
+ /**
+ * Removes the given observer from the observers list.
+ *
+ * @param observer The Observer to receive events.
+ */
+ @MainThread
+ public void removeObserver(@NonNull final Observer<? super T> observer) {
+ assertMainThread("removeObserver");
+ ObserverWrapper removed = mObservers.remove(observer);
+ if (removed == null) {
+ return;
+ }
+ removed.detachObserver();
+ 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.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @MainThread
+ public void removeObservers(@NonNull final LifecycleOwner owner) {
+ assertMainThread("removeObservers");
+ for (Map.Entry<Observer<? super T>, ObserverWrapper> entry : mObservers) {
+ if (entry.getValue().isAttachedTo(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;
+ }
+ ArchTaskExecutor.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
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean hasObservers() {
+ return mObservers.size() > 0;
+ }
+
+ /**
+ * Returns true if this LiveData has active observers.
+ *
+ * @return true if this LiveData has active observers
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean hasActiveObservers() {
+ return mActiveCount > 0;
+ }
+
+ class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
+ @NonNull final LifecycleOwner mOwner;
+
+ LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
+ super(observer);
+ mOwner = owner;
+ }
+
+ @Override
+ boolean shouldBeActive() {
+ return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
+ if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
+ removeObserver(mObserver);
+ return;
+ }
+ activeStateChanged(shouldBeActive());
+ }
+
+ @Override
+ boolean isAttachedTo(LifecycleOwner owner) {
+ return mOwner == owner;
+ }
+
+ @Override
+ void detachObserver() {
+ mOwner.getLifecycle().removeObserver(this);
+ }
+ }
+
+ private abstract class ObserverWrapper {
+ final Observer<? super T> mObserver;
+ boolean mActive;
+ int mLastVersion = START_VERSION;
+
+ ObserverWrapper(Observer<? super T> observer) {
+ mObserver = observer;
+ }
+
+ abstract boolean shouldBeActive();
+
+ boolean isAttachedTo(LifecycleOwner owner) {
+ return false;
+ }
+
+ void detachObserver() {
+ }
+
+ void activeStateChanged(boolean newActive) {
+ if (newActive == mActive) {
+ return;
+ }
+ // immediately set active state, so we'd never dispatch anything to inactive
+ // owner
+ mActive = newActive;
+ boolean wasInactive = LiveData.this.mActiveCount == 0;
+ LiveData.this.mActiveCount += mActive ? 1 : -1;
+ if (wasInactive && mActive) {
+ onActive();
+ }
+ if (LiveData.this.mActiveCount == 0 && !mActive) {
+ onInactive();
+ }
+ if (mActive) {
+ dispatchingValue(this);
+ }
+ }
+ }
+
+ private class AlwaysActiveObserver extends ObserverWrapper {
+
+ AlwaysActiveObserver(Observer<? super T> observer) {
+ super(observer);
+ }
+
+ @Override
+ boolean shouldBeActive() {
+ return true;
+ }
+ }
+
+ private static void assertMainThread(String methodName) {
+ if (!ArchTaskExecutor.getInstance().isMainThread()) {
+ throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
+ + " thread");
+ }
+ }
+}
diff --git a/lifecycle/livedata-core/src/main/java/androidx/lifecycle/MutableLiveData.java b/lifecycle/livedata-core/src/main/java/androidx/lifecycle/MutableLiveData.java
new file mode 100644
index 0000000..06014f0
--- /dev/null
+++ b/lifecycle/livedata-core/src/main/java/androidx/lifecycle/MutableLiveData.java
@@ -0,0 +1,35 @@
+/*
+ * 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 androidx.lifecycle;
+
+/**
+ * {@link LiveData} which publicly exposes {@link #setValue(T)} and {@link #postValue(T)} method.
+ *
+ * @param <T> The type of data hold by this instance
+ */
+@SuppressWarnings("WeakerAccess")
+public class MutableLiveData<T> extends LiveData<T> {
+ @Override
+ public void postValue(T value) {
+ super.postValue(value);
+ }
+
+ @Override
+ public void setValue(T value) {
+ super.setValue(value);
+ }
+}
diff --git a/lifecycle/livedata-core/src/main/java/androidx/lifecycle/Observer.java b/lifecycle/livedata-core/src/main/java/androidx/lifecycle/Observer.java
new file mode 100644
index 0000000..1a7130d
--- /dev/null
+++ b/lifecycle/livedata-core/src/main/java/androidx/lifecycle/Observer.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 androidx.lifecycle;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A simple callback that can receive from {@link LiveData}.
+ *
+ * @param <T> The type of the parameter
+ *
+ * @see LiveData LiveData - for a usage description.
+ */
+public interface Observer<T> {
+ /**
+ * Called when the data is changed.
+ * @param t The new data
+ */
+ void onChanged(@Nullable T t);
+}
diff --git a/lifecycle/livedata-core/src/test/java/android/arch/lifecycle/LiveDataTest.java b/lifecycle/livedata-core/src/test/java/android/arch/lifecycle/LiveDataTest.java
deleted file mode 100644
index 046059b..0000000
--- a/lifecycle/livedata-core/src/test/java/android/arch/lifecycle/LiveDataTest.java
+++ /dev/null
@@ -1,842 +0,0 @@
-/*
- * 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 org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.only;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.testing.InstantTaskExecutorRule;
-import android.support.annotation.Nullable;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
-import org.mockito.Mockito;
-
-@SuppressWarnings({"unchecked"})
-@RunWith(JUnit4.class)
-public class LiveDataTest {
-
- @Rule
- public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
-
- private PublicLiveData<String> mLiveData;
- private MethodExec mActiveObserversChanged;
-
- private LifecycleOwner mOwner;
- private LifecycleRegistry mRegistry;
-
- private LifecycleOwner mOwner2;
- private LifecycleRegistry mRegistry2;
-
- private LifecycleOwner mOwner3;
- private Lifecycle mLifecycle3;
- private Observer<String> mObserver3;
-
- private LifecycleOwner mOwner4;
- private Lifecycle mLifecycle4;
- private Observer<String> mObserver4;
-
- private boolean mInObserver;
-
- @Before
- public void init() {
- mLiveData = new PublicLiveData<>();
-
- mActiveObserversChanged = mock(MethodExec.class);
- mLiveData.activeObserversChanged = mActiveObserversChanged;
-
- mOwner = mock(LifecycleOwner.class);
- mRegistry = new LifecycleRegistry(mOwner);
- when(mOwner.getLifecycle()).thenReturn(mRegistry);
-
- mOwner2 = mock(LifecycleOwner.class);
- mRegistry2 = new LifecycleRegistry(mOwner2);
- when(mOwner2.getLifecycle()).thenReturn(mRegistry2);
-
- mInObserver = false;
- }
-
- @Before
- public void initNonLifecycleRegistry() {
- mOwner3 = mock(LifecycleOwner.class);
- mLifecycle3 = mock(Lifecycle.class);
- mObserver3 = (Observer<String>) mock(Observer.class);
- when(mOwner3.getLifecycle()).thenReturn(mLifecycle3);
-
- mOwner4 = mock(LifecycleOwner.class);
- mLifecycle4 = mock(Lifecycle.class);
- mObserver4 = (Observer<String>) mock(Observer.class);
- when(mOwner4.getLifecycle()).thenReturn(mLifecycle4);
- }
-
- @After
- public void removeExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-
- @Test
- public void testObserverToggle() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
-
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(false));
-
- mLiveData.removeObserver(observer);
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- assertThat(mLiveData.hasObservers(), is(false));
- assertThat(mLiveData.hasActiveObservers(), is(false));
- }
-
- @Test
- public void testActiveObserverToggle() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
-
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(false));
-
- mRegistry.handleLifecycleEvent(ON_START);
- verify(mActiveObserversChanged).onCall(true);
- assertThat(mLiveData.hasActiveObservers(), is(true));
- reset(mActiveObserversChanged);
-
- mRegistry.handleLifecycleEvent(ON_STOP);
- verify(mActiveObserversChanged).onCall(false);
- assertThat(mLiveData.hasActiveObservers(), is(false));
- assertThat(mLiveData.hasObservers(), is(true));
-
- reset(mActiveObserversChanged);
- mRegistry.handleLifecycleEvent(ON_START);
- verify(mActiveObserversChanged).onCall(true);
- assertThat(mLiveData.hasActiveObservers(), is(true));
- assertThat(mLiveData.hasObservers(), is(true));
-
- reset(mActiveObserversChanged);
- mLiveData.removeObserver(observer);
- verify(mActiveObserversChanged).onCall(false);
- assertThat(mLiveData.hasActiveObservers(), is(false));
- assertThat(mLiveData.hasObservers(), is(false));
-
- verifyNoMoreInteractions(mActiveObserversChanged);
- }
-
- @Test
- public void testReAddSameObserverTuple() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
- mLiveData.observe(mOwner, observer);
- assertThat(mLiveData.hasObservers(), is(true));
- }
-
- @Test
- public void testAdd2ObserversWithSameOwnerAndRemove() {
- Observer<String> o1 = (Observer<String>) mock(Observer.class);
- Observer<String> o2 = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, o1);
- mLiveData.observe(mOwner, o2);
- assertThat(mLiveData.hasObservers(), is(true));
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
-
- mRegistry.handleLifecycleEvent(ON_START);
- verify(mActiveObserversChanged).onCall(true);
- mLiveData.setValue("a");
- verify(o1).onChanged("a");
- verify(o2).onChanged("a");
-
- mLiveData.removeObservers(mOwner);
-
- assertThat(mLiveData.hasObservers(), is(false));
- assertThat(mRegistry.getObserverCount(), is(0));
- }
-
- @Test
- public void testAddSameObserverIn2LifecycleOwners() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
-
- mLiveData.observe(mOwner, observer);
- Throwable throwable = null;
- try {
- mLiveData.observe(mOwner2, observer);
- } catch (Throwable t) {
- throwable = t;
- }
- assertThat(throwable, instanceOf(IllegalArgumentException.class));
- //noinspection ConstantConditions
- assertThat(throwable.getMessage(),
- is("Cannot add the same observer with different lifecycles"));
- }
-
- @Test
- public void testRemoveDestroyedObserver() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
- mRegistry.handleLifecycleEvent(ON_START);
- verify(mActiveObserversChanged).onCall(true);
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(true));
-
- reset(mActiveObserversChanged);
-
- mRegistry.handleLifecycleEvent(ON_DESTROY);
- assertThat(mLiveData.hasObservers(), is(false));
- assertThat(mLiveData.hasActiveObservers(), is(false));
- verify(mActiveObserversChanged).onCall(false);
- }
-
- @Test
- public void testInactiveRegistry() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mRegistry.handleLifecycleEvent(ON_DESTROY);
- mLiveData.observe(mOwner, observer);
- assertThat(mLiveData.hasObservers(), is(false));
- }
-
- @Test
- public void testNotifyActiveInactive() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mRegistry.handleLifecycleEvent(ON_CREATE);
- mLiveData.observe(mOwner, observer);
- mLiveData.setValue("a");
- verify(observer, never()).onChanged(anyString());
- mRegistry.handleLifecycleEvent(ON_START);
- verify(observer).onChanged("a");
-
- mLiveData.setValue("b");
- verify(observer).onChanged("b");
-
- mRegistry.handleLifecycleEvent(ON_STOP);
- mLiveData.setValue("c");
- verify(observer, never()).onChanged("c");
-
- mRegistry.handleLifecycleEvent(ON_START);
- verify(observer).onChanged("c");
-
- reset(observer);
- mRegistry.handleLifecycleEvent(ON_STOP);
- mRegistry.handleLifecycleEvent(ON_START);
- verify(observer, never()).onChanged(anyString());
- }
-
- @Test
- public void testStopObservingOwner_onDestroy() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mRegistry.handleLifecycleEvent(ON_CREATE);
- mLiveData.observe(mOwner, observer);
- assertThat(mRegistry.getObserverCount(), is(1));
- mRegistry.handleLifecycleEvent(ON_DESTROY);
- assertThat(mRegistry.getObserverCount(), is(0));
- }
-
- @Test
- public void testStopObservingOwner_onStopObserving() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mRegistry.handleLifecycleEvent(ON_CREATE);
- mLiveData.observe(mOwner, observer);
- assertThat(mRegistry.getObserverCount(), is(1));
-
- mLiveData.removeObserver(observer);
- assertThat(mRegistry.getObserverCount(), is(0));
- }
-
- @Test
- public void testActiveChangeInCallback() {
- mRegistry.handleLifecycleEvent(ON_START);
- Observer<String> observer1 = spy(new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- mRegistry.handleLifecycleEvent(ON_STOP);
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(false));
- }
- });
- final Observer observer2 = mock(Observer.class);
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner, observer2);
- mLiveData.setValue("bla");
- verify(observer1).onChanged(anyString());
- verify(observer2, Mockito.never()).onChanged(anyString());
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(false));
- }
-
- @Test
- public void testActiveChangeInCallback2() {
- Observer<String> observer1 = spy(new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- assertThat(mInObserver, is(false));
- mInObserver = true;
- mRegistry.handleLifecycleEvent(ON_START);
- assertThat(mLiveData.hasActiveObservers(), is(true));
- mInObserver = false;
- }
- });
- final Observer observer2 = spy(new FailReentranceObserver());
- mLiveData.observeForever(observer1);
- mLiveData.observe(mOwner, observer2);
- mLiveData.setValue("bla");
- verify(observer1).onChanged(anyString());
- verify(observer2).onChanged(anyString());
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(true));
- }
-
- @Test
- public void testObserverRemovalInCallback() {
- mRegistry.handleLifecycleEvent(ON_START);
- Observer<String> observer = spy(new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- assertThat(mLiveData.hasObservers(), is(true));
- mLiveData.removeObserver(this);
- assertThat(mLiveData.hasObservers(), is(false));
- }
- });
- mLiveData.observe(mOwner, observer);
- mLiveData.setValue("bla");
- verify(observer).onChanged(anyString());
- assertThat(mLiveData.hasObservers(), is(false));
- }
-
- @Test
- public void testObserverAdditionInCallback() {
- mRegistry.handleLifecycleEvent(ON_START);
- final Observer observer2 = spy(new FailReentranceObserver());
- Observer<String> observer1 = spy(new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- assertThat(mInObserver, is(false));
- mInObserver = true;
- mLiveData.observe(mOwner, observer2);
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(true));
- mInObserver = false;
- }
- });
- mLiveData.observe(mOwner, observer1);
- mLiveData.setValue("bla");
- verify(observer1).onChanged(anyString());
- verify(observer2).onChanged(anyString());
- assertThat(mLiveData.hasObservers(), is(true));
- assertThat(mLiveData.hasActiveObservers(), is(true));
- }
-
- @Test
- public void testObserverWithoutLifecycleOwner() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.setValue("boring");
- mLiveData.observeForever(observer);
- verify(mActiveObserversChanged).onCall(true);
- verify(observer).onChanged("boring");
- mLiveData.setValue("tihs");
- verify(observer).onChanged("tihs");
- mLiveData.removeObserver(observer);
- verify(mActiveObserversChanged).onCall(false);
- mLiveData.setValue("boring");
- reset(observer);
- verify(observer, never()).onChanged(anyString());
- }
-
- @Test
- public void testSetValueDuringSetValue() {
- mRegistry.handleLifecycleEvent(ON_START);
- final Observer observer1 = spy(new Observer<String>() {
- @Override
- public void onChanged(String o) {
- assertThat(mInObserver, is(false));
- mInObserver = true;
- if (o.equals(("bla"))) {
- mLiveData.setValue("gt");
- }
- mInObserver = false;
- }
- });
- final Observer observer2 = spy(new FailReentranceObserver());
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner, observer2);
- mLiveData.setValue("bla");
- verify(observer1, Mockito.atMost(2)).onChanged("gt");
- verify(observer2, Mockito.atMost(2)).onChanged("gt");
- }
-
- @Test
- public void testRemoveDuringSetValue() {
- mRegistry.handleLifecycleEvent(ON_START);
- final Observer observer1 = spy(new Observer<String>() {
- @Override
- public void onChanged(String o) {
- mLiveData.removeObserver(this);
- }
- });
- Observer<String> observer2 = (Observer<String>) mock(Observer.class);
- mLiveData.observeForever(observer1);
- mLiveData.observe(mOwner, observer2);
- mLiveData.setValue("gt");
- verify(observer2).onChanged("gt");
- }
-
- @Test
- public void testDataChangeDuringStateChange() {
- mRegistry.handleLifecycleEvent(ON_START);
- mRegistry.addObserver(new LifecycleObserver() {
- @OnLifecycleEvent(ON_STOP)
- public void onStop() {
- // change data in onStop, observer should not be called!
- mLiveData.setValue("b");
- }
- });
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.setValue("a");
- mLiveData.observe(mOwner, observer);
- verify(observer).onChanged("a");
- mRegistry.handleLifecycleEvent(ON_PAUSE);
- mRegistry.handleLifecycleEvent(ON_STOP);
- verify(observer, never()).onChanged("b");
-
- mRegistry.handleLifecycleEvent(ON_RESUME);
- verify(observer).onChanged("b");
- }
-
- @Test
- public void testNotCallInactiveWithObserveForever() {
- mRegistry.handleLifecycleEvent(ON_START);
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- Observer<String> observer2 = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
- mLiveData.observeForever(observer2);
- verify(mActiveObserversChanged).onCall(true);
- reset(mActiveObserversChanged);
- mRegistry.handleLifecycleEvent(ON_STOP);
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- mRegistry.handleLifecycleEvent(ON_START);
- 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();
- }
-
- @Test
- public void setValue_neverActive_observerOnChangedNotCalled() {
- Observer<String> observer = (Observer<String>) mock(Observer.class);
- mLiveData.observe(mOwner, observer);
-
- mLiveData.setValue("1");
-
- verify(observer, never()).onChanged(anyString());
- }
-
- @Test
- public void setValue_twoObserversTwoStartedOwners_onChangedCalledOnBoth() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
-
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
-
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
-
- mLiveData.setValue("1");
-
- verify(observer1).onChanged("1");
- verify(observer2).onChanged("1");
- }
-
- @Test
- public void setValue_twoObserversOneStartedOwner_onChangedCalledOnOneCorrectObserver() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
-
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
-
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
-
- mLiveData.setValue("1");
-
- verify(observer1).onChanged("1");
- verify(observer2, never()).onChanged(anyString());
- }
-
- @Test
- public void setValue_twoObserversBothStartedAfterSetValue_onChangedCalledOnBoth() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
-
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
-
- mLiveData.setValue("1");
-
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
-
- verify(observer1).onChanged("1");
- verify(observer1).onChanged("1");
- }
-
- @Test
- public void setValue_twoObserversOneStartedAfterSetValue_onChangedCalledOnCorrectObserver() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
-
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
-
- mLiveData.setValue("1");
-
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
-
- verify(observer1).onChanged("1");
- verify(observer2, never()).onChanged(anyString());
- }
-
- @Test
- public void setValue_twoObserversOneStarted_liveDataBecomesActive() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
-
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
-
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
-
- verify(mActiveObserversChanged).onCall(true);
- }
-
- @Test
- public void setValue_twoObserversOneStopped_liveDataStaysActive() {
- Observer<String> observer1 = mock(Observer.class);
- Observer<String> observer2 = mock(Observer.class);
-
- mLiveData.observe(mOwner, observer1);
- mLiveData.observe(mOwner2, observer2);
-
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
-
- verify(mActiveObserversChanged).onCall(true);
-
- reset(mActiveObserversChanged);
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
-
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- }
-
- @Test
- public void setValue_lifecycleIsCreatedNoEvent_liveDataBecomesInactiveAndObserverNotCalled() {
-
- // Arrange.
-
- mLiveData.observe(mOwner3, mObserver3);
-
- GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
-
- reset(mActiveObserversChanged);
- reset(mObserver3);
-
- // Act.
-
- mLiveData.setValue("1");
-
- // Assert.
-
- verify(mActiveObserversChanged).onCall(false);
- verify(mObserver3, never()).onChanged(anyString());
- }
-
- /*
- * Arrange: LiveData was made inactive via SetValue (because the Lifecycle it was
- * observing was in the CREATED state and no event was dispatched).
- * Act: Lifecycle enters Started state and dispatches event.
- * Assert: LiveData becomes active and dispatches new value to observer.
- */
- @Test
- public void test_liveDataInactiveViaSetValueThenLifecycleResumes() {
-
- // Arrange.
-
- mLiveData.observe(mOwner3, mObserver3);
-
- GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
- mLiveData.setValue("1");
-
- reset(mActiveObserversChanged);
- reset(mObserver3);
-
- // Act.
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
-
- // Assert.
-
- verify(mActiveObserversChanged).onCall(true);
- verify(mObserver3).onChanged("1");
- }
-
- /*
- * Arrange: One of two Lifecycles enter the CREATED state without sending an event.
- * Act: Lifecycle's setValue method is called with new value.
- * Assert: LiveData stays active and new value is dispatched to Lifecycle that is still at least
- * STARTED.
- */
- @Test
- public void setValue_oneOfTwoLifecyclesAreCreatedNoEvent() {
-
- // Arrange.
-
- mLiveData.observe(mOwner3, mObserver3);
- mLiveData.observe(mOwner4, mObserver4);
-
- GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
- GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
- lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
-
- reset(mActiveObserversChanged);
- reset(mObserver3);
- reset(mObserver4);
-
- // Act.
-
- mLiveData.setValue("1");
-
- // Assert.
-
- verify(mActiveObserversChanged, never()).onCall(anyBoolean());
- verify(mObserver3, never()).onChanged(anyString());
- verify(mObserver4).onChanged("1");
- }
-
- /*
- * Arrange: Two observed Lifecycles enter the CREATED state without sending an event.
- * Act: Lifecycle's setValue method is called with new value.
- * Assert: LiveData becomes inactive and nothing is dispatched to either observer.
- */
- @Test
- public void setValue_twoLifecyclesAreCreatedNoEvent() {
-
- // Arrange.
-
- mLiveData.observe(mOwner3, mObserver3);
- mLiveData.observe(mOwner4, mObserver4);
-
- GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
- GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
- lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
- when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
-
- reset(mActiveObserversChanged);
- reset(mObserver3);
- reset(mObserver4);
-
- // Act.
-
- mLiveData.setValue("1");
-
- // Assert.
-
- verify(mActiveObserversChanged).onCall(false);
- verify(mObserver3, never()).onChanged(anyString());
- verify(mObserver3, never()).onChanged(anyString());
- }
-
- /*
- * Arrange: LiveData was made inactive via SetValue (because both Lifecycles it was
- * observing were in the CREATED state and no event was dispatched).
- * Act: One Lifecycle enters STARTED state and dispatches lifecycle event.
- * Assert: LiveData becomes active and dispatches new value to observer associated with started
- * Lifecycle.
- */
- @Test
- public void test_liveDataInactiveViaSetValueThenOneLifecycleResumes() {
-
- // Arrange.
-
- mLiveData.observe(mOwner3, mObserver3);
- mLiveData.observe(mOwner4, mObserver4);
-
- GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
- GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
- lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
- when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
-
- mLiveData.setValue("1");
-
- reset(mActiveObserversChanged);
- reset(mObserver3);
- reset(mObserver4);
-
- // Act.
-
- when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
- lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
-
- // Assert.
-
- verify(mActiveObserversChanged).onCall(true);
- verify(mObserver3).onChanged("1");
- verify(mObserver4, never()).onChanged(anyString());
- }
-
- @Test
- public void nestedForeverObserver() {
- mLiveData.setValue(".");
- mLiveData.observeForever(new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- mLiveData.observeForever(mock(Observer.class));
- mLiveData.removeObserver(this);
- }
- });
- verify(mActiveObserversChanged, only()).onCall(true);
- }
-
- @Test
- public void readdForeverObserver() {
- Observer observer = mock(Observer.class);
- mLiveData.observeForever(observer);
- mLiveData.observeForever(observer);
- mLiveData.removeObserver(observer);
- assertThat(mLiveData.hasObservers(), is(false));
- }
-
- private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) {
- ArgumentCaptor<GenericLifecycleObserver> captor =
- ArgumentCaptor.forClass(GenericLifecycleObserver.class);
- verify(lifecycle).addObserver(captor.capture());
- return (captor.getValue());
- }
-
- @SuppressWarnings("WeakerAccess")
- static class PublicLiveData<T> extends LiveData<T> {
- // cannot spy due to internal calls
- public MethodExec activeObserversChanged;
-
- @Override
- protected void onActive() {
- if (activeObserversChanged != null) {
- activeObserversChanged.onCall(true);
- }
- }
-
- @Override
- protected void onInactive() {
- if (activeObserversChanged != null) {
- activeObserversChanged.onCall(false);
- }
- }
- }
-
- private class FailReentranceObserver<T> implements Observer<T> {
- @Override
- public void onChanged(@Nullable T t) {
- assertThat(mInObserver, is(false));
- }
- }
-
- interface MethodExec {
- void onCall(boolean value);
- }
-}
diff --git a/lifecycle/livedata-core/src/test/java/android/arch/lifecycle/ThreadedLiveDataTest.java b/lifecycle/livedata-core/src/test/java/android/arch/lifecycle/ThreadedLiveDataTest.java
deleted file mode 100644
index 3366641..0000000
--- a/lifecycle/livedata-core/src/test/java/android/arch/lifecycle/ThreadedLiveDataTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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_START;
-
-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 android.arch.core.executor.JunitTaskExecutorRule;
-import android.arch.core.executor.TaskExecutor;
-import android.support.annotation.Nullable;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(JUnit4.class)
-public class ThreadedLiveDataTest {
-
- private static final int TIMEOUT_SECS = 3;
-
- @Rule
- public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1, false);
-
- private LiveData<String> mLiveData;
- private LifecycleOwner mLifecycleOwner;
- private LifecycleRegistry mRegistry;
-
- @Before
- public void init() {
- mLiveData = new MutableLiveData<>();
- mLifecycleOwner = mock(LifecycleOwner.class);
- mRegistry = new LifecycleRegistry(mLifecycleOwner);
- when(mLifecycleOwner.getLifecycle()).thenReturn(mRegistry);
- }
-
- @Test
- public void testPostValue() throws InterruptedException {
- final TaskExecutor taskExecutor = mTaskExecutorRule.getTaskExecutor();
- final CountDownLatch finishTestLatch = new CountDownLatch(1);
- final Observer<String> observer = new Observer<String>() {
- @Override
- public void onChanged(@Nullable String newValue) {
- try {
- assertThat(taskExecutor.isMainThread(), is(true));
- assertThat(newValue, is("success"));
- } finally {
- finishTestLatch.countDown();
- }
- }
- };
- taskExecutor.executeOnMainThread(new Runnable() {
- @Override
- public void run() {
- mRegistry.handleLifecycleEvent(ON_START);
- mLiveData.observe(mLifecycleOwner, observer);
- final CountDownLatch latch = new CountDownLatch(1);
- taskExecutor.executeOnDiskIO(new Runnable() {
- @Override
- public void run() {
- mLiveData.postValue("fail");
- mLiveData.postValue("success");
- latch.countDown();
- }
- });
- try {
- assertThat(latch.await(TIMEOUT_SECS, TimeUnit.SECONDS), is(true));
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- });
- assertThat(finishTestLatch.await(TIMEOUT_SECS, TimeUnit.SECONDS), is(true));
- }
-}
diff --git a/lifecycle/livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java b/lifecycle/livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
new file mode 100644
index 0000000..741ee66
--- /dev/null
+++ b/lifecycle/livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
@@ -0,0 +1,842 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+@SuppressWarnings({"unchecked"})
+@RunWith(JUnit4.class)
+public class LiveDataTest {
+
+ @Rule
+ public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private PublicLiveData<String> mLiveData;
+ private MethodExec mActiveObserversChanged;
+
+ private LifecycleOwner mOwner;
+ private LifecycleRegistry mRegistry;
+
+ private LifecycleOwner mOwner2;
+ private LifecycleRegistry mRegistry2;
+
+ private LifecycleOwner mOwner3;
+ private Lifecycle mLifecycle3;
+ private Observer<String> mObserver3;
+
+ private LifecycleOwner mOwner4;
+ private Lifecycle mLifecycle4;
+ private Observer<String> mObserver4;
+
+ private boolean mInObserver;
+
+ @Before
+ public void init() {
+ mLiveData = new PublicLiveData<>();
+
+ mActiveObserversChanged = mock(MethodExec.class);
+ mLiveData.activeObserversChanged = mActiveObserversChanged;
+
+ mOwner = mock(LifecycleOwner.class);
+ mRegistry = new LifecycleRegistry(mOwner);
+ when(mOwner.getLifecycle()).thenReturn(mRegistry);
+
+ mOwner2 = mock(LifecycleOwner.class);
+ mRegistry2 = new LifecycleRegistry(mOwner2);
+ when(mOwner2.getLifecycle()).thenReturn(mRegistry2);
+
+ mInObserver = false;
+ }
+
+ @Before
+ public void initNonLifecycleRegistry() {
+ mOwner3 = mock(LifecycleOwner.class);
+ mLifecycle3 = mock(Lifecycle.class);
+ mObserver3 = (Observer<String>) mock(Observer.class);
+ when(mOwner3.getLifecycle()).thenReturn(mLifecycle3);
+
+ mOwner4 = mock(LifecycleOwner.class);
+ mLifecycle4 = mock(Lifecycle.class);
+ mObserver4 = (Observer<String>) mock(Observer.class);
+ when(mOwner4.getLifecycle()).thenReturn(mLifecycle4);
+ }
+
+ @After
+ public void removeExecutorDelegate() {
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+
+ @Test
+ public void testObserverToggle() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, observer);
+
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+
+ mLiveData.removeObserver(observer);
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ assertThat(mLiveData.hasObservers(), is(false));
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ }
+
+ @Test
+ public void testActiveObserverToggle() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, observer);
+
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(mActiveObserversChanged).onCall(true);
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+ reset(mActiveObserversChanged);
+
+ mRegistry.handleLifecycleEvent(ON_STOP);
+ verify(mActiveObserversChanged).onCall(false);
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ assertThat(mLiveData.hasObservers(), is(true));
+
+ reset(mActiveObserversChanged);
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(mActiveObserversChanged).onCall(true);
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+ assertThat(mLiveData.hasObservers(), is(true));
+
+ reset(mActiveObserversChanged);
+ mLiveData.removeObserver(observer);
+ verify(mActiveObserversChanged).onCall(false);
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ assertThat(mLiveData.hasObservers(), is(false));
+
+ verifyNoMoreInteractions(mActiveObserversChanged);
+ }
+
+ @Test
+ public void testReAddSameObserverTuple() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, observer);
+ mLiveData.observe(mOwner, observer);
+ assertThat(mLiveData.hasObservers(), is(true));
+ }
+
+ @Test
+ public void testAdd2ObserversWithSameOwnerAndRemove() {
+ Observer<String> o1 = (Observer<String>) mock(Observer.class);
+ Observer<String> o2 = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, o1);
+ mLiveData.observe(mOwner, o2);
+ assertThat(mLiveData.hasObservers(), is(true));
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(mActiveObserversChanged).onCall(true);
+ mLiveData.setValue("a");
+ verify(o1).onChanged("a");
+ verify(o2).onChanged("a");
+
+ mLiveData.removeObservers(mOwner);
+
+ assertThat(mLiveData.hasObservers(), is(false));
+ assertThat(mRegistry.getObserverCount(), is(0));
+ }
+
+ @Test
+ public void testAddSameObserverIn2LifecycleOwners() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer);
+ Throwable throwable = null;
+ try {
+ mLiveData.observe(mOwner2, observer);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertThat(throwable, instanceOf(IllegalArgumentException.class));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ is("Cannot add the same observer with different lifecycles"));
+ }
+
+ @Test
+ public void testRemoveDestroyedObserver() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, observer);
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(mActiveObserversChanged).onCall(true);
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+
+ reset(mActiveObserversChanged);
+
+ mRegistry.handleLifecycleEvent(ON_DESTROY);
+ assertThat(mLiveData.hasObservers(), is(false));
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ verify(mActiveObserversChanged).onCall(false);
+ }
+
+ @Test
+ public void testInactiveRegistry() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mRegistry.handleLifecycleEvent(ON_DESTROY);
+ mLiveData.observe(mOwner, observer);
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+
+ @Test
+ public void testNotifyActiveInactive() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mRegistry.handleLifecycleEvent(ON_CREATE);
+ mLiveData.observe(mOwner, observer);
+ mLiveData.setValue("a");
+ verify(observer, never()).onChanged(anyString());
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(observer).onChanged("a");
+
+ mLiveData.setValue("b");
+ verify(observer).onChanged("b");
+
+ mRegistry.handleLifecycleEvent(ON_STOP);
+ mLiveData.setValue("c");
+ verify(observer, never()).onChanged("c");
+
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(observer).onChanged("c");
+
+ reset(observer);
+ mRegistry.handleLifecycleEvent(ON_STOP);
+ mRegistry.handleLifecycleEvent(ON_START);
+ verify(observer, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void testStopObservingOwner_onDestroy() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mRegistry.handleLifecycleEvent(ON_CREATE);
+ mLiveData.observe(mOwner, observer);
+ assertThat(mRegistry.getObserverCount(), is(1));
+ mRegistry.handleLifecycleEvent(ON_DESTROY);
+ assertThat(mRegistry.getObserverCount(), is(0));
+ }
+
+ @Test
+ public void testStopObservingOwner_onStopObserving() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mRegistry.handleLifecycleEvent(ON_CREATE);
+ mLiveData.observe(mOwner, observer);
+ assertThat(mRegistry.getObserverCount(), is(1));
+
+ mLiveData.removeObserver(observer);
+ assertThat(mRegistry.getObserverCount(), is(0));
+ }
+
+ @Test
+ public void testActiveChangeInCallback() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ Observer<String> observer1 = spy(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mRegistry.handleLifecycleEvent(ON_STOP);
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ }
+ });
+ final Observer observer2 = mock(Observer.class);
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner, observer2);
+ mLiveData.setValue("bla");
+ verify(observer1).onChanged(anyString());
+ verify(observer2, Mockito.never()).onChanged(anyString());
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(false));
+ }
+
+ @Test
+ public void testActiveChangeInCallback2() {
+ Observer<String> observer1 = spy(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ assertThat(mInObserver, is(false));
+ mInObserver = true;
+ mRegistry.handleLifecycleEvent(ON_START);
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+ mInObserver = false;
+ }
+ });
+ final Observer observer2 = spy(new FailReentranceObserver());
+ mLiveData.observeForever(observer1);
+ mLiveData.observe(mOwner, observer2);
+ mLiveData.setValue("bla");
+ verify(observer1).onChanged(anyString());
+ verify(observer2).onChanged(anyString());
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+ }
+
+ @Test
+ public void testObserverRemovalInCallback() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ Observer<String> observer = spy(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ assertThat(mLiveData.hasObservers(), is(true));
+ mLiveData.removeObserver(this);
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+ });
+ mLiveData.observe(mOwner, observer);
+ mLiveData.setValue("bla");
+ verify(observer).onChanged(anyString());
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+
+ @Test
+ public void testObserverAdditionInCallback() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ final Observer observer2 = spy(new FailReentranceObserver());
+ Observer<String> observer1 = spy(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ assertThat(mInObserver, is(false));
+ mInObserver = true;
+ mLiveData.observe(mOwner, observer2);
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+ mInObserver = false;
+ }
+ });
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.setValue("bla");
+ verify(observer1).onChanged(anyString());
+ verify(observer2).onChanged(anyString());
+ assertThat(mLiveData.hasObservers(), is(true));
+ assertThat(mLiveData.hasActiveObservers(), is(true));
+ }
+
+ @Test
+ public void testObserverWithoutLifecycleOwner() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.setValue("boring");
+ mLiveData.observeForever(observer);
+ verify(mActiveObserversChanged).onCall(true);
+ verify(observer).onChanged("boring");
+ mLiveData.setValue("tihs");
+ verify(observer).onChanged("tihs");
+ mLiveData.removeObserver(observer);
+ verify(mActiveObserversChanged).onCall(false);
+ mLiveData.setValue("boring");
+ reset(observer);
+ verify(observer, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void testSetValueDuringSetValue() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ final Observer observer1 = spy(new Observer<String>() {
+ @Override
+ public void onChanged(String o) {
+ assertThat(mInObserver, is(false));
+ mInObserver = true;
+ if (o.equals(("bla"))) {
+ mLiveData.setValue("gt");
+ }
+ mInObserver = false;
+ }
+ });
+ final Observer observer2 = spy(new FailReentranceObserver());
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner, observer2);
+ mLiveData.setValue("bla");
+ verify(observer1, Mockito.atMost(2)).onChanged("gt");
+ verify(observer2, Mockito.atMost(2)).onChanged("gt");
+ }
+
+ @Test
+ public void testRemoveDuringSetValue() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ final Observer observer1 = spy(new Observer<String>() {
+ @Override
+ public void onChanged(String o) {
+ mLiveData.removeObserver(this);
+ }
+ });
+ Observer<String> observer2 = (Observer<String>) mock(Observer.class);
+ mLiveData.observeForever(observer1);
+ mLiveData.observe(mOwner, observer2);
+ mLiveData.setValue("gt");
+ verify(observer2).onChanged("gt");
+ }
+
+ @Test
+ public void testDataChangeDuringStateChange() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ mRegistry.addObserver(new LifecycleObserver() {
+ @OnLifecycleEvent(ON_STOP)
+ public void onStop() {
+ // change data in onStop, observer should not be called!
+ mLiveData.setValue("b");
+ }
+ });
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.setValue("a");
+ mLiveData.observe(mOwner, observer);
+ verify(observer).onChanged("a");
+ mRegistry.handleLifecycleEvent(ON_PAUSE);
+ mRegistry.handleLifecycleEvent(ON_STOP);
+ verify(observer, never()).onChanged("b");
+
+ mRegistry.handleLifecycleEvent(ON_RESUME);
+ verify(observer).onChanged("b");
+ }
+
+ @Test
+ public void testNotCallInactiveWithObserveForever() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ Observer<String> observer2 = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, observer);
+ mLiveData.observeForever(observer2);
+ verify(mActiveObserversChanged).onCall(true);
+ reset(mActiveObserversChanged);
+ mRegistry.handleLifecycleEvent(ON_STOP);
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ mRegistry.handleLifecycleEvent(ON_START);
+ 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();
+ }
+
+ @Test
+ public void setValue_neverActive_observerOnChangedNotCalled() {
+ Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mLiveData.observe(mOwner, observer);
+
+ mLiveData.setValue("1");
+
+ verify(observer, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void setValue_twoObserversTwoStartedOwners_onChangedCalledOnBoth() {
+ Observer<String> observer1 = mock(Observer.class);
+ Observer<String> observer2 = mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner2, observer2);
+
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ mLiveData.setValue("1");
+
+ verify(observer1).onChanged("1");
+ verify(observer2).onChanged("1");
+ }
+
+ @Test
+ public void setValue_twoObserversOneStartedOwner_onChangedCalledOnOneCorrectObserver() {
+ Observer<String> observer1 = mock(Observer.class);
+ Observer<String> observer2 = mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner2, observer2);
+
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ mLiveData.setValue("1");
+
+ verify(observer1).onChanged("1");
+ verify(observer2, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void setValue_twoObserversBothStartedAfterSetValue_onChangedCalledOnBoth() {
+ Observer<String> observer1 = mock(Observer.class);
+ Observer<String> observer2 = mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner2, observer2);
+
+ mLiveData.setValue("1");
+
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ verify(observer1).onChanged("1");
+ verify(observer1).onChanged("1");
+ }
+
+ @Test
+ public void setValue_twoObserversOneStartedAfterSetValue_onChangedCalledOnCorrectObserver() {
+ Observer<String> observer1 = mock(Observer.class);
+ Observer<String> observer2 = mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner2, observer2);
+
+ mLiveData.setValue("1");
+
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ verify(observer1).onChanged("1");
+ verify(observer2, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void setValue_twoObserversOneStarted_liveDataBecomesActive() {
+ Observer<String> observer1 = mock(Observer.class);
+ Observer<String> observer2 = mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner2, observer2);
+
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ verify(mActiveObserversChanged).onCall(true);
+ }
+
+ @Test
+ public void setValue_twoObserversOneStopped_liveDataStaysActive() {
+ Observer<String> observer1 = mock(Observer.class);
+ Observer<String> observer2 = mock(Observer.class);
+
+ mLiveData.observe(mOwner, observer1);
+ mLiveData.observe(mOwner2, observer2);
+
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ verify(mActiveObserversChanged).onCall(true);
+
+ reset(mActiveObserversChanged);
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ }
+
+ @Test
+ public void setValue_lifecycleIsCreatedNoEvent_liveDataBecomesInactiveAndObserverNotCalled() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+
+ GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+
+ // Act.
+
+ mLiveData.setValue("1");
+
+ // Assert.
+
+ verify(mActiveObserversChanged).onCall(false);
+ verify(mObserver3, never()).onChanged(anyString());
+ }
+
+ /*
+ * Arrange: LiveData was made inactive via SetValue (because the Lifecycle it was
+ * observing was in the CREATED state and no event was dispatched).
+ * Act: Lifecycle enters Started state and dispatches event.
+ * Assert: LiveData becomes active and dispatches new value to observer.
+ */
+ @Test
+ public void test_liveDataInactiveViaSetValueThenLifecycleResumes() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+
+ GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+ mLiveData.setValue("1");
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+
+ // Act.
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ // Assert.
+
+ verify(mActiveObserversChanged).onCall(true);
+ verify(mObserver3).onChanged("1");
+ }
+
+ /*
+ * Arrange: One of two Lifecycles enter the CREATED state without sending an event.
+ * Act: Lifecycle's setValue method is called with new value.
+ * Assert: LiveData stays active and new value is dispatched to Lifecycle that is still at least
+ * STARTED.
+ */
+ @Test
+ public void setValue_oneOfTwoLifecyclesAreCreatedNoEvent() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+ mLiveData.observe(mOwner4, mObserver4);
+
+ GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
+ GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+ lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+ reset(mObserver4);
+
+ // Act.
+
+ mLiveData.setValue("1");
+
+ // Assert.
+
+ verify(mActiveObserversChanged, never()).onCall(anyBoolean());
+ verify(mObserver3, never()).onChanged(anyString());
+ verify(mObserver4).onChanged("1");
+ }
+
+ /*
+ * Arrange: Two observed Lifecycles enter the CREATED state without sending an event.
+ * Act: Lifecycle's setValue method is called with new value.
+ * Assert: LiveData becomes inactive and nothing is dispatched to either observer.
+ */
+ @Test
+ public void setValue_twoLifecyclesAreCreatedNoEvent() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+ mLiveData.observe(mOwner4, mObserver4);
+
+ GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
+ GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+ lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+ reset(mObserver4);
+
+ // Act.
+
+ mLiveData.setValue("1");
+
+ // Assert.
+
+ verify(mActiveObserversChanged).onCall(false);
+ verify(mObserver3, never()).onChanged(anyString());
+ verify(mObserver3, never()).onChanged(anyString());
+ }
+
+ /*
+ * Arrange: LiveData was made inactive via SetValue (because both Lifecycles it was
+ * observing were in the CREATED state and no event was dispatched).
+ * Act: One Lifecycle enters STARTED state and dispatches lifecycle event.
+ * Assert: LiveData becomes active and dispatches new value to observer associated with started
+ * Lifecycle.
+ */
+ @Test
+ public void test_liveDataInactiveViaSetValueThenOneLifecycleResumes() {
+
+ // Arrange.
+
+ mLiveData.observe(mOwner3, mObserver3);
+ mLiveData.observe(mOwner4, mObserver4);
+
+ GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3);
+ GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+ lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START);
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+ when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED);
+
+ mLiveData.setValue("1");
+
+ reset(mActiveObserversChanged);
+ reset(mObserver3);
+ reset(mObserver4);
+
+ // Act.
+
+ when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED);
+ lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START);
+
+ // Assert.
+
+ verify(mActiveObserversChanged).onCall(true);
+ verify(mObserver3).onChanged("1");
+ verify(mObserver4, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void nestedForeverObserver() {
+ mLiveData.setValue(".");
+ mLiveData.observeForever(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveData.observeForever(mock(Observer.class));
+ mLiveData.removeObserver(this);
+ }
+ });
+ verify(mActiveObserversChanged, only()).onCall(true);
+ }
+
+ @Test
+ public void readdForeverObserver() {
+ Observer observer = mock(Observer.class);
+ mLiveData.observeForever(observer);
+ mLiveData.observeForever(observer);
+ mLiveData.removeObserver(observer);
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+
+ private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) {
+ ArgumentCaptor<GenericLifecycleObserver> captor =
+ ArgumentCaptor.forClass(GenericLifecycleObserver.class);
+ verify(lifecycle).addObserver(captor.capture());
+ return (captor.getValue());
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class PublicLiveData<T> extends LiveData<T> {
+ // cannot spy due to internal calls
+ public MethodExec activeObserversChanged;
+
+ @Override
+ protected void onActive() {
+ if (activeObserversChanged != null) {
+ activeObserversChanged.onCall(true);
+ }
+ }
+
+ @Override
+ protected void onInactive() {
+ if (activeObserversChanged != null) {
+ activeObserversChanged.onCall(false);
+ }
+ }
+ }
+
+ private class FailReentranceObserver<T> implements Observer<T> {
+ @Override
+ public void onChanged(@Nullable T t) {
+ assertThat(mInObserver, is(false));
+ }
+ }
+
+ interface MethodExec {
+ void onCall(boolean value);
+ }
+}
diff --git a/lifecycle/livedata-core/src/test/java/androidx/lifecycle/ThreadedLiveDataTest.java b/lifecycle/livedata-core/src/test/java/androidx/lifecycle/ThreadedLiveDataTest.java
new file mode 100644
index 0000000..19dcd21
--- /dev/null
+++ b/lifecycle/livedata-core/src/test/java/androidx/lifecycle/ThreadedLiveDataTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+
+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 androidx.annotation.Nullable;
+import androidx.arch.core.executor.JunitTaskExecutorRule;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class ThreadedLiveDataTest {
+
+ private static final int TIMEOUT_SECS = 3;
+
+ @Rule
+ public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1, false);
+
+ private LiveData<String> mLiveData;
+ private LifecycleOwner mLifecycleOwner;
+ private LifecycleRegistry mRegistry;
+
+ @Before
+ public void init() {
+ mLiveData = new MutableLiveData<>();
+ mLifecycleOwner = mock(LifecycleOwner.class);
+ mRegistry = new LifecycleRegistry(mLifecycleOwner);
+ when(mLifecycleOwner.getLifecycle()).thenReturn(mRegistry);
+ }
+
+ @Test
+ public void testPostValue() throws InterruptedException {
+ final TaskExecutor taskExecutor = mTaskExecutorRule.getTaskExecutor();
+ final CountDownLatch finishTestLatch = new CountDownLatch(1);
+ final Observer<String> observer = new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String newValue) {
+ try {
+ assertThat(taskExecutor.isMainThread(), is(true));
+ assertThat(newValue, is("success"));
+ } finally {
+ finishTestLatch.countDown();
+ }
+ }
+ };
+ taskExecutor.executeOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ mRegistry.handleLifecycleEvent(ON_START);
+ mLiveData.observe(mLifecycleOwner, observer);
+ final CountDownLatch latch = new CountDownLatch(1);
+ taskExecutor.executeOnDiskIO(new Runnable() {
+ @Override
+ public void run() {
+ mLiveData.postValue("fail");
+ mLiveData.postValue("success");
+ latch.countDown();
+ }
+ });
+ try {
+ assertThat(latch.await(TIMEOUT_SECS, TimeUnit.SECONDS), is(true));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ assertThat(finishTestLatch.await(TIMEOUT_SECS, TimeUnit.SECONDS), is(true));
+ }
+}
diff --git a/lifecycle/livedata/api/current.txt b/lifecycle/livedata/api/current.txt
index 13f8154..88ef93c 100644
--- a/lifecycle/livedata/api/current.txt
+++ b/lifecycle/livedata/api/current.txt
@@ -1,14 +1,14 @@
-package android.arch.lifecycle {
+package androidx.lifecycle {
- public class MediatorLiveData<T> extends android.arch.lifecycle.MutableLiveData {
+ public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData {
ctor public MediatorLiveData();
- method public <S> void addSource(android.arch.lifecycle.LiveData<S>, android.arch.lifecycle.Observer<? super S>);
- method public <S> void removeSource(android.arch.lifecycle.LiveData<S>);
+ method public <S> void addSource(androidx.lifecycle.LiveData<S>, androidx.lifecycle.Observer<? super S>);
+ method public <S> void removeSource(androidx.lifecycle.LiveData<S>);
}
public class Transformations {
- method public static <X, Y> android.arch.lifecycle.LiveData<Y> map(android.arch.lifecycle.LiveData<X>, android.arch.core.util.Function<X, Y>);
- method public static <X, Y> android.arch.lifecycle.LiveData<Y> switchMap(android.arch.lifecycle.LiveData<X>, android.arch.core.util.Function<X, android.arch.lifecycle.LiveData<Y>>);
+ method public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X, Y>);
+ method public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X, androidx.lifecycle.LiveData<Y>>);
}
}
diff --git a/lifecycle/livedata/api/1.1.0.txt b/lifecycle/livedata/api_legacy/1.1.0.txt
similarity index 100%
rename from lifecycle/livedata/api/1.1.0.txt
rename to lifecycle/livedata/api_legacy/1.1.0.txt
diff --git a/lifecycle/livedata/api_legacy/current.txt b/lifecycle/livedata/api_legacy/current.txt
new file mode 100644
index 0000000..13f8154
--- /dev/null
+++ b/lifecycle/livedata/api_legacy/current.txt
@@ -0,0 +1,15 @@
+package android.arch.lifecycle {
+
+ public class MediatorLiveData<T> extends android.arch.lifecycle.MutableLiveData {
+ ctor public MediatorLiveData();
+ method public <S> void addSource(android.arch.lifecycle.LiveData<S>, android.arch.lifecycle.Observer<? super S>);
+ method public <S> void removeSource(android.arch.lifecycle.LiveData<S>);
+ }
+
+ public class Transformations {
+ method public static <X, Y> android.arch.lifecycle.LiveData<Y> map(android.arch.lifecycle.LiveData<X>, android.arch.core.util.Function<X, Y>);
+ method public static <X, Y> android.arch.lifecycle.LiveData<Y> switchMap(android.arch.lifecycle.LiveData<X>, android.arch.core.util.Function<X, android.arch.lifecycle.LiveData<Y>>);
+ }
+
+}
+
diff --git a/lifecycle/livedata/build.gradle b/lifecycle/livedata/build.gradle
index cf3b50c..d797335 100644
--- a/lifecycle/livedata/build.gradle
+++ b/lifecycle/livedata/build.gradle
@@ -24,11 +24,11 @@
}
dependencies {
- implementation(project(":arch:common"))
- api(project(":arch:runtime"))
- api(project(":lifecycle:livedata-core"))
+ implementation(project(":arch:core-common"))
+ api(project(":arch:core-runtime"))
+ api(project(":lifecycle:lifecycle-livedata-core"))
- testImplementation(project(":lifecycle:runtime"))
+ testImplementation(project(":lifecycle:lifecycle-runtime"))
testImplementation(project(":arch:core-testing"))
testImplementation(JUNIT)
testImplementation(MOCKITO_CORE)
diff --git a/lifecycle/livedata/src/main/AndroidManifest.xml b/lifecycle/livedata/src/main/AndroidManifest.xml
index 9f738ee..ecd0fe1 100644
--- a/lifecycle/livedata/src/main/AndroidManifest.xml
+++ b/lifecycle/livedata/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.livedata">
+ package="androidx.lifecycle.livedata">
</manifest>
diff --git a/lifecycle/livedata/src/main/java/android/arch/lifecycle/ComputableLiveData.java b/lifecycle/livedata/src/main/java/android/arch/lifecycle/ComputableLiveData.java
deleted file mode 100644
index d9e1ed5..0000000
--- a/lifecycle/livedata/src/main/java/android/arch/lifecycle/ComputableLiveData.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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.arch.core.executor.ArchTaskExecutor;
-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.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A LiveData class that can be invalidated & computed when there are active observers.
- * <p>
- * It can be invalidated via {@link #invalidate()}, which will result in a call to
- * {@link #compute()} if there are active observers (or when they start observing)
- * <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)
-public abstract class ComputableLiveData<T> {
-
- private final Executor mExecutor;
- private final LiveData<T> mLiveData;
-
- private AtomicBoolean mInvalid = new AtomicBoolean(true);
- private AtomicBoolean mComputing = new AtomicBoolean(false);
-
- /**
- * Creates a computable live data that computes values on the arch IO thread executor.
- */
- @SuppressWarnings("WeakerAccess")
- public ComputableLiveData() {
- this(ArchTaskExecutor.getIOThreadExecutor());
- }
-
- /**
- *
- * Creates a computable live data that computes values on the specified executor.
- *
- * @param executor Executor that is used to compute new LiveData values.
- */
- @SuppressWarnings("WeakerAccess")
- public ComputableLiveData(@NonNull Executor executor) {
- mExecutor = executor;
- mLiveData = new LiveData<T>() {
- @Override
- protected void onActive() {
- mExecutor.execute(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) {
- mExecutor.execute(mRefreshRunnable);
- }
- }
- }
- };
-
- /**
- * Invalidates the LiveData.
- * <p>
- * When there are active observers, this will trigger a call to {@link #compute()}.
- */
- public void invalidate() {
- ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
- }
-
- @SuppressWarnings("WeakerAccess")
- @WorkerThread
- protected abstract T compute();
-}
diff --git a/lifecycle/livedata/src/main/java/android/arch/lifecycle/MediatorLiveData.java b/lifecycle/livedata/src/main/java/android/arch/lifecycle/MediatorLiveData.java
deleted file mode 100644
index 3960163..0000000
--- a/lifecycle/livedata/src/main/java/android/arch/lifecycle/MediatorLiveData.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * 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.arch.core.internal.SafeIterableMap;
-import android.support.annotation.CallSuper;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.Map;
-
-/**
- * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on
- * {@code OnChanged} events from them.
- * <p>
- * This class correctly propagates its active/inactive states down to source {@code LiveData}
- * objects.
- * <p>
- * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them
- * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object:
- * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for
- * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback
- * is called for either of them, we set a new value in {@code liveDataMerger}.
- *
- * <pre>
- * LiveData<Integer> liveData1 = ...;
- * LiveData<Integer> liveData2 = ...;
- *
- * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
- * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
- * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
- * </pre>
- * <p>
- * Let's consider that we only want 10 values emitted by {@code liveData1}, to be
- * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code
- * liveData1} and remove it as a source.
- * <pre>
- * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
- * private int count = 1;
- *
- * {@literal @}Override public void onChanged(@Nullable Integer s) {
- * count++;
- * liveDataMerger.setValue(s);
- * if (count > 10) {
- * liveDataMerger.removeSource(liveData1);
- * }
- * }
- * });
- * </pre>
- *
- * @param <T> The type of data hold by this instance
- */
-@SuppressWarnings("WeakerAccess")
-public class MediatorLiveData<T> extends MutableLiveData<T> {
- private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
-
- /**
- * Starts to listen the given {@code source} LiveData, {@code onChanged} observer will be called
- * when {@code source} value was changed.
- * <p>
- * {@code onChanged} callback will be called only when this {@code MediatorLiveData} is active.
- * <p> If the given LiveData is already added as a source but with a different Observer,
- * {@link IllegalArgumentException} will be thrown.
- *
- * @param source the {@code LiveData} to listen to
- * @param onChanged The observer that will receive the events
- * @param <S> The type of data hold by {@code source} LiveData
- */
- @MainThread
- public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
- Source<S> e = new Source<>(source, onChanged);
- Source<?> existing = mSources.putIfAbsent(source, e);
- if (existing != null && existing.mObserver != onChanged) {
- throw new IllegalArgumentException(
- "This source was already added with the different observer");
- }
- if (existing != null) {
- return;
- }
- if (hasActiveObservers()) {
- e.plug();
- }
- }
-
- /**
- * Stops to listen the given {@code LiveData}.
- *
- * @param toRemote {@code LiveData} to stop to listen
- * @param <S> the type of data hold by {@code source} LiveData
- */
- @MainThread
- public <S> void removeSource(@NonNull LiveData<S> toRemote) {
- Source<?> source = mSources.remove(toRemote);
- if (source != null) {
- source.unplug();
- }
- }
-
- @CallSuper
- @Override
- protected void onActive() {
- for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
- source.getValue().plug();
- }
- }
-
- @CallSuper
- @Override
- protected void onInactive() {
- for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
- source.getValue().unplug();
- }
- }
-
- private static class Source<V> implements Observer<V> {
- final LiveData<V> mLiveData;
- final Observer<? super V> mObserver;
- int mVersion = START_VERSION;
-
- Source(LiveData<V> liveData, final Observer<? super V> observer) {
- mLiveData = liveData;
- mObserver = observer;
- }
-
- void plug() {
- mLiveData.observeForever(this);
- }
-
- void unplug() {
- mLiveData.removeObserver(this);
- }
-
- @Override
- public void onChanged(@Nullable V v) {
- if (mVersion != mLiveData.getVersion()) {
- mVersion = mLiveData.getVersion();
- mObserver.onChanged(v);
- }
- }
- }
-}
diff --git a/lifecycle/livedata/src/main/java/android/arch/lifecycle/Transformations.java b/lifecycle/livedata/src/main/java/android/arch/lifecycle/Transformations.java
deleted file mode 100644
index c735f8b..0000000
--- a/lifecycle/livedata/src/main/java/android/arch/lifecycle/Transformations.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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.arch.core.util.Function;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-/**
- * 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 {
-
- private Transformations() {
- }
-
- /**
- * Applies the given function on the main thread to each value emitted by {@code source}
- * 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
- * @param <X> a type of {@code source} LiveData
- * @param <Y> a type of resulting LiveData.
- * @return a LiveData which emits resulting values
- */
- @MainThread
- public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
- @NonNull final Function<X, Y> func) {
- final MediatorLiveData<Y> result = new MediatorLiveData<>();
- result.addSource(source, new Observer<X>() {
- @Override
- public void onChanged(@Nullable X x) {
- result.setValue(func.apply(x));
- }
- });
- return result;
- }
-
- /**
- * Creates a LiveData, let's name it {@code swLiveData}, which follows next flow:
- * it reacts on changes of {@code trigger} LiveData, applies the given function to new value of
- * {@code trigger} LiveData and sets resulting LiveData as a "backing" LiveData
- * to {@code swLiveData}.
- * "Backing" LiveData means, that all events emitted by it will retransmitted
- * by {@code swLiveData}.
- * <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
- * @param <Y> a type of resulting LiveData
- */
- @MainThread
- public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
- @NonNull final Function<X, LiveData<Y>> func) {
- final MediatorLiveData<Y> result = new MediatorLiveData<>();
- result.addSource(trigger, new Observer<X>() {
- LiveData<Y> mSource;
-
- @Override
- public void onChanged(@Nullable X x) {
- LiveData<Y> newLiveData = func.apply(x);
- if (mSource == newLiveData) {
- return;
- }
- if (mSource != null) {
- result.removeSource(mSource);
- }
- mSource = newLiveData;
- if (mSource != null) {
- result.addSource(mSource, new Observer<Y>() {
- @Override
- public void onChanged(@Nullable Y y) {
- result.setValue(y);
- }
- });
- }
- }
- });
- return result;
- }
-}
diff --git a/lifecycle/livedata/src/main/java/androidx/lifecycle/ComputableLiveData.java b/lifecycle/livedata/src/main/java/androidx/lifecycle/ComputableLiveData.java
new file mode 100644
index 0000000..e2e2a1b
--- /dev/null
+++ b/lifecycle/livedata/src/main/java/androidx/lifecycle/ComputableLiveData.java
@@ -0,0 +1,148 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A LiveData class that can be invalidated & computed when there are active observers.
+ * <p>
+ * It can be invalidated via {@link #invalidate()}, which will result in a call to
+ * {@link #compute()} if there are active observers (or when they start observing)
+ * <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)
+public abstract class ComputableLiveData<T> {
+
+ private final Executor mExecutor;
+ private final LiveData<T> mLiveData;
+
+ private AtomicBoolean mInvalid = new AtomicBoolean(true);
+ private AtomicBoolean mComputing = new AtomicBoolean(false);
+
+ /**
+ * Creates a computable live data that computes values on the arch IO thread executor.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public ComputableLiveData() {
+ this(ArchTaskExecutor.getIOThreadExecutor());
+ }
+
+ /**
+ *
+ * Creates a computable live data that computes values on the specified executor.
+ *
+ * @param executor Executor that is used to compute new LiveData values.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public ComputableLiveData(@NonNull Executor executor) {
+ mExecutor = executor;
+ mLiveData = new LiveData<T>() {
+ @Override
+ protected void onActive() {
+ mExecutor.execute(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) {
+ mExecutor.execute(mRefreshRunnable);
+ }
+ }
+ }
+ };
+
+ /**
+ * Invalidates the LiveData.
+ * <p>
+ * When there are active observers, this will trigger a call to {@link #compute()}.
+ */
+ public void invalidate() {
+ ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @WorkerThread
+ protected abstract T compute();
+}
diff --git a/lifecycle/livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java b/lifecycle/livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java
new file mode 100644
index 0000000..58261dd
--- /dev/null
+++ b/lifecycle/livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java
@@ -0,0 +1,156 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.internal.SafeIterableMap;
+
+import java.util.Map;
+
+/**
+ * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on
+ * {@code OnChanged} events from them.
+ * <p>
+ * This class correctly propagates its active/inactive states down to source {@code LiveData}
+ * objects.
+ * <p>
+ * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them
+ * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object:
+ * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for
+ * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback
+ * is called for either of them, we set a new value in {@code liveDataMerger}.
+ *
+ * <pre>
+ * LiveData<Integer> liveData1 = ...;
+ * LiveData<Integer> liveData2 = ...;
+ *
+ * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
+ * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
+ * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
+ * </pre>
+ * <p>
+ * Let's consider that we only want 10 values emitted by {@code liveData1}, to be
+ * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code
+ * liveData1} and remove it as a source.
+ * <pre>
+ * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
+ * private int count = 1;
+ *
+ * {@literal @}Override public void onChanged(@Nullable Integer s) {
+ * count++;
+ * liveDataMerger.setValue(s);
+ * if (count > 10) {
+ * liveDataMerger.removeSource(liveData1);
+ * }
+ * }
+ * });
+ * </pre>
+ *
+ * @param <T> The type of data hold by this instance
+ */
+@SuppressWarnings("WeakerAccess")
+public class MediatorLiveData<T> extends MutableLiveData<T> {
+ private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
+
+ /**
+ * Starts to listen the given {@code source} LiveData, {@code onChanged} observer will be called
+ * when {@code source} value was changed.
+ * <p>
+ * {@code onChanged} callback will be called only when this {@code MediatorLiveData} is active.
+ * <p> If the given LiveData is already added as a source but with a different Observer,
+ * {@link IllegalArgumentException} will be thrown.
+ *
+ * @param source the {@code LiveData} to listen to
+ * @param onChanged The observer that will receive the events
+ * @param <S> The type of data hold by {@code source} LiveData
+ */
+ @MainThread
+ public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
+ Source<S> e = new Source<>(source, onChanged);
+ Source<?> existing = mSources.putIfAbsent(source, e);
+ if (existing != null && existing.mObserver != onChanged) {
+ throw new IllegalArgumentException(
+ "This source was already added with the different observer");
+ }
+ if (existing != null) {
+ return;
+ }
+ if (hasActiveObservers()) {
+ e.plug();
+ }
+ }
+
+ /**
+ * Stops to listen the given {@code LiveData}.
+ *
+ * @param toRemote {@code LiveData} to stop to listen
+ * @param <S> the type of data hold by {@code source} LiveData
+ */
+ @MainThread
+ public <S> void removeSource(@NonNull LiveData<S> toRemote) {
+ Source<?> source = mSources.remove(toRemote);
+ if (source != null) {
+ source.unplug();
+ }
+ }
+
+ @CallSuper
+ @Override
+ protected void onActive() {
+ for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
+ source.getValue().plug();
+ }
+ }
+
+ @CallSuper
+ @Override
+ protected void onInactive() {
+ for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
+ source.getValue().unplug();
+ }
+ }
+
+ private static class Source<V> implements Observer<V> {
+ final LiveData<V> mLiveData;
+ final Observer<? super V> mObserver;
+ int mVersion = START_VERSION;
+
+ Source(LiveData<V> liveData, final Observer<? super V> observer) {
+ mLiveData = liveData;
+ mObserver = observer;
+ }
+
+ void plug() {
+ mLiveData.observeForever(this);
+ }
+
+ void unplug() {
+ mLiveData.removeObserver(this);
+ }
+
+ @Override
+ public void onChanged(@Nullable V v) {
+ if (mVersion != mLiveData.getVersion()) {
+ mVersion = mLiveData.getVersion();
+ mObserver.onChanged(v);
+ }
+ }
+ }
+}
diff --git a/lifecycle/livedata/src/main/java/androidx/lifecycle/Transformations.java b/lifecycle/livedata/src/main/java/androidx/lifecycle/Transformations.java
new file mode 100644
index 0000000..392c3de
--- /dev/null
+++ b/lifecycle/livedata/src/main/java/androidx/lifecycle/Transformations.java
@@ -0,0 +1,153 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+
+/**
+ * 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 {
+
+ private Transformations() {
+ }
+
+ /**
+ * Applies the given function on the main thread to each value emitted by {@code source}
+ * 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
+ * @param <X> a type of {@code source} LiveData
+ * @param <Y> a type of resulting LiveData.
+ * @return a LiveData which emits resulting values
+ */
+ @MainThread
+ public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
+ @NonNull final Function<X, Y> func) {
+ final MediatorLiveData<Y> result = new MediatorLiveData<>();
+ result.addSource(source, new Observer<X>() {
+ @Override
+ public void onChanged(@Nullable X x) {
+ result.setValue(func.apply(x));
+ }
+ });
+ return result;
+ }
+
+ /**
+ * Creates a LiveData, let's name it {@code swLiveData}, which follows next flow:
+ * it reacts on changes of {@code trigger} LiveData, applies the given function to new value of
+ * {@code trigger} LiveData and sets resulting LiveData as a "backing" LiveData
+ * to {@code swLiveData}.
+ * "Backing" LiveData means, that all events emitted by it will retransmitted
+ * by {@code swLiveData}.
+ * <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
+ * @param <Y> a type of resulting LiveData
+ */
+ @MainThread
+ public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
+ @NonNull final Function<X, LiveData<Y>> func) {
+ final MediatorLiveData<Y> result = new MediatorLiveData<>();
+ result.addSource(trigger, new Observer<X>() {
+ LiveData<Y> mSource;
+
+ @Override
+ public void onChanged(@Nullable X x) {
+ LiveData<Y> newLiveData = func.apply(x);
+ if (mSource == newLiveData) {
+ return;
+ }
+ if (mSource != null) {
+ result.removeSource(mSource);
+ }
+ mSource = newLiveData;
+ if (mSource != null) {
+ result.addSource(mSource, new Observer<Y>() {
+ @Override
+ public void onChanged(@Nullable Y y) {
+ result.setValue(y);
+ }
+ });
+ }
+ }
+ });
+ return result;
+ }
+}
diff --git a/lifecycle/livedata/src/test/java/android/arch/lifecycle/ComputableLiveDataTest.java b/lifecycle/livedata/src/test/java/android/arch/lifecycle/ComputableLiveDataTest.java
deleted file mode 100644
index 9130b25..0000000
--- a/lifecycle/livedata/src/test/java/android/arch/lifecycle/ComputableLiveDataTest.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.TaskExecutor;
-import android.arch.core.executor.TaskExecutorWithFakeMainThread;
-import android.arch.lifecycle.util.InstantTaskExecutor;
-import android.support.annotation.NonNull;
-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.ArgumentCaptor;
-
-import java.util.Collections;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-@RunWith(JUnit4.class)
-public class ComputableLiveDataTest {
- private TaskExecutor mTaskExecutor;
- private TestLifecycleOwner mLifecycleOwner;
-
- @Before
- public void setup() {
- mLifecycleOwner = new TestLifecycleOwner();
- }
-
- @Before
- public void swapExecutorDelegate() {
- mTaskExecutor = spy(new InstantTaskExecutor());
- ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
- }
-
- @After
- public void removeExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-
- @Test
- public void noComputeWithoutObservers() {
- final TestComputable computable = new TestComputable();
- verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
- verify(mTaskExecutor, never()).executeOnDiskIO(computable.mInvalidationRunnable);
- }
-
- @Test
- public void noConcurrentCompute() throws InterruptedException {
- TaskExecutorWithFakeMainThread executor = new TaskExecutorWithFakeMainThread(2);
- ArchTaskExecutor.getInstance().setDelegate(executor);
- try {
- // # of compute calls
- final Semaphore computeCounter = new Semaphore(0);
- // available permits for computation
- final Semaphore computeLock = new Semaphore(0);
- final TestComputable computable = new TestComputable(1, 2) {
- @Override
- protected Integer compute() {
- try {
- computeCounter.release(1);
- computeLock.tryAcquire(1, 20, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- throw new AssertionError(e);
- }
- return super.compute();
- }
- };
- final ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
- //noinspection unchecked
- final Observer<Integer> observer = mock(Observer.class);
- executor.postToMainThread(new Runnable() {
- @Override
- public void run() {
- computable.getLiveData().observeForever(observer);
- verify(observer, never()).onChanged(anyInt());
- }
- });
- // wait for first compute call
- assertThat(computeCounter.tryAcquire(1, 2, TimeUnit.SECONDS), is(true));
- // re-invalidate while in compute
- computable.invalidate();
- computable.invalidate();
- computable.invalidate();
- computable.invalidate();
- // ensure another compute call does not arrive
- assertThat(computeCounter.tryAcquire(1, 2, TimeUnit.SECONDS), is(false));
- // allow computation to finish
- computeLock.release(2);
- // wait for the second result, first will be skipped due to invalidation during compute
- verify(observer, timeout(2000)).onChanged(captor.capture());
- assertThat(captor.getAllValues(), is(Collections.singletonList(2)));
- reset(observer);
- // allow all computations to run, there should not be any.
- computeLock.release(100);
- // unfortunately, Mockito.after is not available in 1.9.5
- executor.drainTasks(2);
- // assert no other results arrive
- verify(observer, never()).onChanged(anyInt());
- } finally {
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
- }
-
- @Test
- public void addingObserverShouldTriggerAComputation() {
- TestComputable computable = new TestComputable(1);
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_CREATE);
- final AtomicInteger mValue = new AtomicInteger(-1);
- computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
- @Override
- public void onChanged(@Nullable Integer integer) {
- //noinspection ConstantConditions
- mValue.set(integer);
- }
- });
- verify(mTaskExecutor, never()).executeOnDiskIO(any(Runnable.class));
- assertThat(mValue.get(), is(-1));
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- verify(mTaskExecutor).executeOnDiskIO(computable.mRefreshRunnable);
- assertThat(mValue.get(), is(1));
- }
-
- @Test
- public void customExecutor() {
- Executor customExecutor = mock(Executor.class);
- TestComputable computable = new TestComputable(customExecutor, 1);
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_CREATE);
- computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
- @Override
- public void onChanged(@Nullable Integer integer) {
- // ignored
- }
- });
- verify(mTaskExecutor, never()).executeOnDiskIO(any(Runnable.class));
- verify(customExecutor, never()).execute(any(Runnable.class));
-
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
-
- verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
- verify(customExecutor).execute(computable.mRefreshRunnable);
- }
-
- @Test
- public void invalidationShouldNotReTriggerComputationIfObserverIsInActive() {
- TestComputable computable = new TestComputable(1, 2);
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- final AtomicInteger mValue = new AtomicInteger(-1);
- computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
- @Override
- public void onChanged(@Nullable Integer integer) {
- //noinspection ConstantConditions
- mValue.set(integer);
- }
- });
- assertThat(mValue.get(), is(1));
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_STOP);
- computable.invalidate();
- reset(mTaskExecutor);
- verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
- assertThat(mValue.get(), is(1));
- }
-
- @Test
- public void invalidationShouldReTriggerQueryIfObserverIsActive() {
- TestComputable computable = new TestComputable(1, 2);
- mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- final AtomicInteger mValue = new AtomicInteger(-1);
- computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
- @Override
- public void onChanged(@Nullable Integer integer) {
- //noinspection ConstantConditions
- mValue.set(integer);
- }
- });
- assertThat(mValue.get(), is(1));
- computable.invalidate();
- assertThat(mValue.get(), is(2));
- }
-
- static class TestComputable extends ComputableLiveData<Integer> {
- final int[] mValues;
- AtomicInteger mValueCounter = new AtomicInteger();
-
- TestComputable(@NonNull Executor executor, int... values) {
- super(executor);
- mValues = values;
- }
-
- TestComputable(int... values) {
- mValues = values;
- }
-
- @Override
- protected Integer compute() {
- return mValues[mValueCounter.getAndIncrement()];
- }
- }
-
- static class TestLifecycleOwner implements LifecycleOwner {
- private LifecycleRegistry mLifecycle;
-
- TestLifecycleOwner() {
- mLifecycle = new LifecycleRegistry(this);
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mLifecycle;
- }
-
- void handleEvent(Lifecycle.Event event) {
- mLifecycle.handleLifecycleEvent(event);
- }
- }
-}
diff --git a/lifecycle/livedata/src/test/java/android/arch/lifecycle/MediatorLiveDataTest.java b/lifecycle/livedata/src/test/java/android/arch/lifecycle/MediatorLiveDataTest.java
deleted file mode 100644
index 9dbeb44..0000000
--- a/lifecycle/livedata/src/test/java/android/arch/lifecycle/MediatorLiveDataTest.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.testing.InstantTaskExecutorRule;
-import android.arch.lifecycle.util.InstantTaskExecutor;
-import android.support.annotation.Nullable;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@SuppressWarnings("unchecked")
-@RunWith(JUnit4.class)
-public class MediatorLiveDataTest {
-
- @Rule
- public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
-
- private LifecycleOwner mOwner;
- private LifecycleRegistry mRegistry;
- private MediatorLiveData<String> mMediator;
- private LiveData<String> mSource;
- private boolean mSourceActive;
-
- @Before
- public void setup() {
- mOwner = mock(LifecycleOwner.class);
- mRegistry = new LifecycleRegistry(mOwner);
- when(mOwner.getLifecycle()).thenReturn(mRegistry);
- mMediator = new MediatorLiveData<>();
- mSource = new LiveData<String>() {
- @Override
- protected void onActive() {
- mSourceActive = true;
- }
-
- @Override
- protected void onInactive() {
- mSourceActive = false;
- }
- };
- mSourceActive = false;
- mMediator.observe(mOwner, mock(Observer.class));
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- }
-
- @Before
- public void swapExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
- }
-
- @Test
- public void testSingleDelivery() {
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- mSource.setValue("flatfoot");
- verify(observer).onChanged("flatfoot");
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- reset(observer);
- verify(observer, never()).onChanged(any());
- }
-
- @Test
- public void testChangeWhileInactive() {
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- mMediator.observe(mOwner, mock(Observer.class));
- mSource.setValue("one");
- verify(observer).onChanged("one");
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- reset(observer);
- mSource.setValue("flatfoot");
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- verify(observer).onChanged("flatfoot");
- }
-
-
- @Test
- public void testAddSourceToActive() {
- mSource.setValue("flatfoot");
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- verify(observer).onChanged("flatfoot");
- }
-
- @Test
- public void testAddSourceToInActive() {
- mSource.setValue("flatfoot");
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- verify(observer, never()).onChanged(any());
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- verify(observer).onChanged("flatfoot");
- }
-
- @Test
- public void testRemoveSource() {
- mSource.setValue("flatfoot");
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- verify(observer).onChanged("flatfoot");
- mMediator.removeSource(mSource);
- reset(observer);
- mSource.setValue("failure");
- verify(observer, never()).onChanged(any());
- }
-
- @Test
- public void testSourceInactive() {
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- assertThat(mSourceActive, is(true));
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- assertThat(mSourceActive, is(false));
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- assertThat(mSourceActive, is(true));
- }
-
- @Test
- public void testNoLeakObserver() {
- // Imitates a destruction of a ViewModel: a listener of LiveData is destroyed,
- // a reference to MediatorLiveData is cleaned up. In this case we shouldn't leak
- // MediatorLiveData as an observer of mSource.
- assertThat(mSource.hasObservers(), is(false));
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- assertThat(mSource.hasObservers(), is(true));
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
- mMediator = null;
- assertThat(mSource.hasObservers(), is(false));
- }
-
- @Test
- public void testMultipleSources() {
- Observer observer1 = mock(Observer.class);
- mMediator.addSource(mSource, observer1);
- MutableLiveData<Integer> source2 = new MutableLiveData<>();
- Observer observer2 = mock(Observer.class);
- mMediator.addSource(source2, observer2);
- mSource.setValue("flatfoot");
- verify(observer1).onChanged("flatfoot");
- verify(observer2, never()).onChanged(any());
- reset(observer1, observer2);
- source2.setValue(1703);
- verify(observer1, never()).onChanged(any());
- verify(observer2).onChanged(1703);
- reset(observer1, observer2);
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- mSource.setValue("failure");
- source2.setValue(0);
- verify(observer1, never()).onChanged(any());
- verify(observer2, never()).onChanged(any());
- }
-
- @Test
- public void removeSourceDuringOnActive() {
- // to trigger ConcurrentModificationException,
- // we have to call remove from a collection during "for" loop.
- // ConcurrentModificationException is thrown from next() method of an iterator
- // so this modification shouldn't be at the last iteration,
- // because if it is a last iteration, then next() wouldn't be called.
- // And the last: an order of an iteration over sources is not defined,
- // so I have to call it remove operation from all observers.
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- Observer<String> removingObserver = new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- mMediator.removeSource(mSource);
- }
- };
- mMediator.addSource(mSource, removingObserver);
- MutableLiveData<String> source2 = new MutableLiveData<>();
- source2.setValue("nana");
- mMediator.addSource(source2, removingObserver);
- mSource.setValue("petjack");
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void reAddSameSourceWithDifferentObserver() {
- mMediator.addSource(mSource, mock(Observer.class));
- mMediator.addSource(mSource, mock(Observer.class));
- }
-
- @Test
- public void addSameSourceWithSameObserver() {
- Observer observer = mock(Observer.class);
- mMediator.addSource(mSource, observer);
- mMediator.addSource(mSource, observer);
- // no exception was thrown
- }
-
- @Test
- public void addSourceDuringOnActive() {
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
- mSource.setValue("a");
- mMediator.addSource(mSource, new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- MutableLiveData<String> source = new MutableLiveData<>();
- source.setValue("b");
- mMediator.addSource(source, new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- mMediator.setValue("c");
- }
- });
- }
- });
- mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- assertThat(mMediator.getValue(), is("c"));
- }
-
-}
diff --git a/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java b/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java
deleted file mode 100644
index 02397da..0000000
--- a/lifecycle/livedata/src/test/java/android/arch/lifecycle/TransformationsTest.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.only;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.util.Function;
-import android.arch.lifecycle.util.InstantTaskExecutor;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@SuppressWarnings("unchecked")
-@RunWith(JUnit4.class)
-public class TransformationsTest {
-
- private LifecycleOwner mOwner;
-
- @Before
- public void swapExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
- }
-
- @Before
- public void setup() {
- mOwner = mock(LifecycleOwner.class);
- LifecycleRegistry registry = new LifecycleRegistry(mOwner);
- when(mOwner.getLifecycle()).thenReturn(registry);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
- registry.handleLifecycleEvent(Lifecycle.Event.ON_START);
- }
-
- @Test
- public void testMap() {
- LiveData<String> source = new MutableLiveData<>();
- LiveData<Integer> mapped = Transformations.map(source, new Function<String, Integer>() {
- @Override
- public Integer apply(String input) {
- return input.length();
- }
- });
- Observer<Integer> observer = mock(Observer.class);
- mapped.observe(mOwner, observer);
- source.setValue("four");
- verify(observer).onChanged(4);
- }
-
- @Test
- public void testSwitchMap() {
- LiveData<Integer> trigger = new MutableLiveData<>();
- final LiveData<String> first = new MutableLiveData<>();
- final LiveData<String> second = new MutableLiveData<>();
- LiveData<String> result = Transformations.switchMap(trigger,
- new Function<Integer, LiveData<String>>() {
- @Override
- public LiveData<String> apply(Integer input) {
- if (input == 1) {
- return first;
- } else {
- return second;
- }
- }
- });
-
- Observer<String> observer = mock(Observer.class);
- result.observe(mOwner, observer);
- verify(observer, never()).onChanged(anyString());
- first.setValue("first");
- trigger.setValue(1);
- verify(observer).onChanged("first");
- second.setValue("second");
- reset(observer);
- verify(observer, never()).onChanged(anyString());
- trigger.setValue(2);
- verify(observer).onChanged("second");
- reset(observer);
- first.setValue("failure");
- verify(observer, never()).onChanged(anyString());
- }
-
- @Test
- public void testSwitchMap2() {
- LiveData<Integer> trigger = new MutableLiveData<>();
- final LiveData<String> first = new MutableLiveData<>();
- final LiveData<String> second = new MutableLiveData<>();
- LiveData<String> result = Transformations.switchMap(trigger,
- new Function<Integer, LiveData<String>>() {
- @Override
- public LiveData<String> apply(Integer input) {
- if (input == 1) {
- return first;
- } else {
- return second;
- }
- }
- });
-
- Observer<String> observer = mock(Observer.class);
- result.observe(mOwner, observer);
-
- verify(observer, never()).onChanged(anyString());
- trigger.setValue(1);
- verify(observer, never()).onChanged(anyString());
- first.setValue("fi");
- verify(observer).onChanged("fi");
- first.setValue("rst");
- verify(observer).onChanged("rst");
-
- second.setValue("second");
- reset(observer);
- verify(observer, never()).onChanged(anyString());
- trigger.setValue(2);
- verify(observer).onChanged("second");
- reset(observer);
- first.setValue("failure");
- verify(observer, never()).onChanged(anyString());
- }
-
- @Test
- public void testNoRedispatchSwitchMap() {
- LiveData<Integer> trigger = new MutableLiveData<>();
- final LiveData<String> first = new MutableLiveData<>();
- LiveData<String> result = Transformations.switchMap(trigger,
- new Function<Integer, LiveData<String>>() {
- @Override
- public LiveData<String> apply(Integer input) {
- return first;
- }
- });
-
- Observer<String> observer = mock(Observer.class);
- result.observe(mOwner, observer);
- verify(observer, never()).onChanged(anyString());
- first.setValue("first");
- trigger.setValue(1);
- verify(observer).onChanged("first");
- reset(observer);
- trigger.setValue(2);
- verify(observer, never()).onChanged(anyString());
- }
-
- @Test
- public void testSwitchMapToNull() {
- LiveData<Integer> trigger = new MutableLiveData<>();
- final LiveData<String> first = new MutableLiveData<>();
- LiveData<String> result = Transformations.switchMap(trigger,
- new Function<Integer, LiveData<String>>() {
- @Override
- public LiveData<String> apply(Integer input) {
- if (input == 1) {
- return first;
- } else {
- return null;
- }
- }
- });
-
- Observer<String> observer = mock(Observer.class);
- result.observe(mOwner, observer);
- verify(observer, never()).onChanged(anyString());
- first.setValue("first");
- trigger.setValue(1);
- verify(observer).onChanged("first");
- reset(observer);
-
- trigger.setValue(2);
- verify(observer, never()).onChanged(anyString());
- assertThat(first.hasObservers(), is(false));
- }
-
- @Test
- public void noObsoleteValueTest() {
- MutableLiveData<Integer> numbers = new MutableLiveData<>();
- LiveData<Integer> squared = Transformations.map(numbers, new Function<Integer, Integer>() {
- @Override
- public Integer apply(Integer input) {
- return input * input;
- }
- });
-
- Observer observer = mock(Observer.class);
- squared.setValue(1);
- squared.observeForever(observer);
- verify(observer).onChanged(1);
- squared.removeObserver(observer);
- reset(observer);
- numbers.setValue(2);
- squared.observeForever(observer);
- verify(observer, only()).onChanged(4);
- }
-}
diff --git a/lifecycle/livedata/src/test/java/android/arch/lifecycle/util/InstantTaskExecutor.java b/lifecycle/livedata/src/test/java/android/arch/lifecycle/util/InstantTaskExecutor.java
deleted file mode 100644
index 52318fd..0000000
--- a/lifecycle/livedata/src/test/java/android/arch/lifecycle/util/InstantTaskExecutor.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.util;
-
-import android.arch.core.executor.TaskExecutor;
-
-public class InstantTaskExecutor extends TaskExecutor {
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- runnable.run();
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- runnable.run();
- }
-
- @Override
- public boolean isMainThread() {
- return true;
- }
-}
diff --git a/lifecycle/livedata/src/test/java/androidx/lifecycle/ComputableLiveDataTest.java b/lifecycle/livedata/src/test/java/androidx/lifecycle/ComputableLiveDataTest.java
new file mode 100644
index 0000000..2b9d37e
--- /dev/null
+++ b/lifecycle/livedata/src/test/java/androidx/lifecycle/ComputableLiveDataTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+import androidx.arch.core.executor.TaskExecutorWithFakeMainThread;
+import androidx.lifecycle.util.InstantTaskExecutor;
+
+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.ArgumentCaptor;
+
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(JUnit4.class)
+public class ComputableLiveDataTest {
+ private TaskExecutor mTaskExecutor;
+ private TestLifecycleOwner mLifecycleOwner;
+
+ @Before
+ public void setup() {
+ mLifecycleOwner = new TestLifecycleOwner();
+ }
+
+ @Before
+ public void swapExecutorDelegate() {
+ mTaskExecutor = spy(new InstantTaskExecutor());
+ ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
+ }
+
+ @After
+ public void removeExecutorDelegate() {
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+
+ @Test
+ public void noComputeWithoutObservers() {
+ final TestComputable computable = new TestComputable();
+ verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
+ verify(mTaskExecutor, never()).executeOnDiskIO(computable.mInvalidationRunnable);
+ }
+
+ @Test
+ public void noConcurrentCompute() throws InterruptedException {
+ TaskExecutorWithFakeMainThread executor = new TaskExecutorWithFakeMainThread(2);
+ ArchTaskExecutor.getInstance().setDelegate(executor);
+ try {
+ // # of compute calls
+ final Semaphore computeCounter = new Semaphore(0);
+ // available permits for computation
+ final Semaphore computeLock = new Semaphore(0);
+ final TestComputable computable = new TestComputable(1, 2) {
+ @Override
+ protected Integer compute() {
+ try {
+ computeCounter.release(1);
+ computeLock.tryAcquire(1, 20, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ return super.compute();
+ }
+ };
+ final ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
+ //noinspection unchecked
+ final Observer<Integer> observer = mock(Observer.class);
+ executor.postToMainThread(new Runnable() {
+ @Override
+ public void run() {
+ computable.getLiveData().observeForever(observer);
+ verify(observer, never()).onChanged(anyInt());
+ }
+ });
+ // wait for first compute call
+ assertThat(computeCounter.tryAcquire(1, 2, TimeUnit.SECONDS), is(true));
+ // re-invalidate while in compute
+ computable.invalidate();
+ computable.invalidate();
+ computable.invalidate();
+ computable.invalidate();
+ // ensure another compute call does not arrive
+ assertThat(computeCounter.tryAcquire(1, 2, TimeUnit.SECONDS), is(false));
+ // allow computation to finish
+ computeLock.release(2);
+ // wait for the second result, first will be skipped due to invalidation during compute
+ verify(observer, timeout(2000)).onChanged(captor.capture());
+ assertThat(captor.getAllValues(), is(Collections.singletonList(2)));
+ reset(observer);
+ // allow all computations to run, there should not be any.
+ computeLock.release(100);
+ // unfortunately, Mockito.after is not available in 1.9.5
+ executor.drainTasks(2);
+ // assert no other results arrive
+ verify(observer, never()).onChanged(anyInt());
+ } finally {
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+ }
+
+ @Test
+ public void addingObserverShouldTriggerAComputation() {
+ TestComputable computable = new TestComputable(1);
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_CREATE);
+ final AtomicInteger mValue = new AtomicInteger(-1);
+ computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
+ @Override
+ public void onChanged(@Nullable Integer integer) {
+ //noinspection ConstantConditions
+ mValue.set(integer);
+ }
+ });
+ verify(mTaskExecutor, never()).executeOnDiskIO(any(Runnable.class));
+ assertThat(mValue.get(), is(-1));
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ verify(mTaskExecutor).executeOnDiskIO(computable.mRefreshRunnable);
+ assertThat(mValue.get(), is(1));
+ }
+
+ @Test
+ public void customExecutor() {
+ Executor customExecutor = mock(Executor.class);
+ TestComputable computable = new TestComputable(customExecutor, 1);
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_CREATE);
+ computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
+ @Override
+ public void onChanged(@Nullable Integer integer) {
+ // ignored
+ }
+ });
+ verify(mTaskExecutor, never()).executeOnDiskIO(any(Runnable.class));
+ verify(customExecutor, never()).execute(any(Runnable.class));
+
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+
+ verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
+ verify(customExecutor).execute(computable.mRefreshRunnable);
+ }
+
+ @Test
+ public void invalidationShouldNotReTriggerComputationIfObserverIsInActive() {
+ TestComputable computable = new TestComputable(1, 2);
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final AtomicInteger mValue = new AtomicInteger(-1);
+ computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
+ @Override
+ public void onChanged(@Nullable Integer integer) {
+ //noinspection ConstantConditions
+ mValue.set(integer);
+ }
+ });
+ assertThat(mValue.get(), is(1));
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_STOP);
+ computable.invalidate();
+ reset(mTaskExecutor);
+ verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
+ assertThat(mValue.get(), is(1));
+ }
+
+ @Test
+ public void invalidationShouldReTriggerQueryIfObserverIsActive() {
+ TestComputable computable = new TestComputable(1, 2);
+ mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final AtomicInteger mValue = new AtomicInteger(-1);
+ computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
+ @Override
+ public void onChanged(@Nullable Integer integer) {
+ //noinspection ConstantConditions
+ mValue.set(integer);
+ }
+ });
+ assertThat(mValue.get(), is(1));
+ computable.invalidate();
+ assertThat(mValue.get(), is(2));
+ }
+
+ static class TestComputable extends ComputableLiveData<Integer> {
+ final int[] mValues;
+ AtomicInteger mValueCounter = new AtomicInteger();
+
+ TestComputable(@NonNull Executor executor, int... values) {
+ super(executor);
+ mValues = values;
+ }
+
+ TestComputable(int... values) {
+ mValues = values;
+ }
+
+ @Override
+ protected Integer compute() {
+ return mValues[mValueCounter.getAndIncrement()];
+ }
+ }
+
+ static class TestLifecycleOwner implements LifecycleOwner {
+ private LifecycleRegistry mLifecycle;
+
+ TestLifecycleOwner() {
+ mLifecycle = new LifecycleRegistry(this);
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycle;
+ }
+
+ void handleEvent(Lifecycle.Event event) {
+ mLifecycle.handleLifecycleEvent(event);
+ }
+ }
+}
diff --git a/lifecycle/livedata/src/test/java/androidx/lifecycle/MediatorLiveDataTest.java b/lifecycle/livedata/src/test/java/androidx/lifecycle/MediatorLiveDataTest.java
new file mode 100644
index 0000000..a90c343
--- /dev/null
+++ b/lifecycle/livedata/src/test/java/androidx/lifecycle/MediatorLiveDataTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.util.InstantTaskExecutor;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+public class MediatorLiveDataTest {
+
+ @Rule
+ public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private LifecycleOwner mOwner;
+ private LifecycleRegistry mRegistry;
+ private MediatorLiveData<String> mMediator;
+ private LiveData<String> mSource;
+ private boolean mSourceActive;
+
+ @Before
+ public void setup() {
+ mOwner = mock(LifecycleOwner.class);
+ mRegistry = new LifecycleRegistry(mOwner);
+ when(mOwner.getLifecycle()).thenReturn(mRegistry);
+ mMediator = new MediatorLiveData<>();
+ mSource = new LiveData<String>() {
+ @Override
+ protected void onActive() {
+ mSourceActive = true;
+ }
+
+ @Override
+ protected void onInactive() {
+ mSourceActive = false;
+ }
+ };
+ mSourceActive = false;
+ mMediator.observe(mOwner, mock(Observer.class));
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ }
+
+ @Before
+ public void swapExecutorDelegate() {
+ ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
+ }
+
+ @Test
+ public void testSingleDelivery() {
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ mSource.setValue("flatfoot");
+ verify(observer).onChanged("flatfoot");
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ reset(observer);
+ verify(observer, never()).onChanged(any());
+ }
+
+ @Test
+ public void testChangeWhileInactive() {
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ mMediator.observe(mOwner, mock(Observer.class));
+ mSource.setValue("one");
+ verify(observer).onChanged("one");
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ reset(observer);
+ mSource.setValue("flatfoot");
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ verify(observer).onChanged("flatfoot");
+ }
+
+
+ @Test
+ public void testAddSourceToActive() {
+ mSource.setValue("flatfoot");
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ verify(observer).onChanged("flatfoot");
+ }
+
+ @Test
+ public void testAddSourceToInActive() {
+ mSource.setValue("flatfoot");
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ verify(observer, never()).onChanged(any());
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ verify(observer).onChanged("flatfoot");
+ }
+
+ @Test
+ public void testRemoveSource() {
+ mSource.setValue("flatfoot");
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ verify(observer).onChanged("flatfoot");
+ mMediator.removeSource(mSource);
+ reset(observer);
+ mSource.setValue("failure");
+ verify(observer, never()).onChanged(any());
+ }
+
+ @Test
+ public void testSourceInactive() {
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ assertThat(mSourceActive, is(true));
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ assertThat(mSourceActive, is(false));
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(mSourceActive, is(true));
+ }
+
+ @Test
+ public void testNoLeakObserver() {
+ // Imitates a destruction of a ViewModel: a listener of LiveData is destroyed,
+ // a reference to MediatorLiveData is cleaned up. In this case we shouldn't leak
+ // MediatorLiveData as an observer of mSource.
+ assertThat(mSource.hasObservers(), is(false));
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ assertThat(mSource.hasObservers(), is(true));
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ mMediator = null;
+ assertThat(mSource.hasObservers(), is(false));
+ }
+
+ @Test
+ public void testMultipleSources() {
+ Observer observer1 = mock(Observer.class);
+ mMediator.addSource(mSource, observer1);
+ MutableLiveData<Integer> source2 = new MutableLiveData<>();
+ Observer observer2 = mock(Observer.class);
+ mMediator.addSource(source2, observer2);
+ mSource.setValue("flatfoot");
+ verify(observer1).onChanged("flatfoot");
+ verify(observer2, never()).onChanged(any());
+ reset(observer1, observer2);
+ source2.setValue(1703);
+ verify(observer1, never()).onChanged(any());
+ verify(observer2).onChanged(1703);
+ reset(observer1, observer2);
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ mSource.setValue("failure");
+ source2.setValue(0);
+ verify(observer1, never()).onChanged(any());
+ verify(observer2, never()).onChanged(any());
+ }
+
+ @Test
+ public void removeSourceDuringOnActive() {
+ // to trigger ConcurrentModificationException,
+ // we have to call remove from a collection during "for" loop.
+ // ConcurrentModificationException is thrown from next() method of an iterator
+ // so this modification shouldn't be at the last iteration,
+ // because if it is a last iteration, then next() wouldn't be called.
+ // And the last: an order of an iteration over sources is not defined,
+ // so I have to call it remove operation from all observers.
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ Observer<String> removingObserver = new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mMediator.removeSource(mSource);
+ }
+ };
+ mMediator.addSource(mSource, removingObserver);
+ MutableLiveData<String> source2 = new MutableLiveData<>();
+ source2.setValue("nana");
+ mMediator.addSource(source2, removingObserver);
+ mSource.setValue("petjack");
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void reAddSameSourceWithDifferentObserver() {
+ mMediator.addSource(mSource, mock(Observer.class));
+ mMediator.addSource(mSource, mock(Observer.class));
+ }
+
+ @Test
+ public void addSameSourceWithSameObserver() {
+ Observer observer = mock(Observer.class);
+ mMediator.addSource(mSource, observer);
+ mMediator.addSource(mSource, observer);
+ // no exception was thrown
+ }
+
+ @Test
+ public void addSourceDuringOnActive() {
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+ mSource.setValue("a");
+ mMediator.addSource(mSource, new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ MutableLiveData<String> source = new MutableLiveData<>();
+ source.setValue("b");
+ mMediator.addSource(source, new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mMediator.setValue("c");
+ }
+ });
+ }
+ });
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(mMediator.getValue(), is("c"));
+ }
+
+}
diff --git a/lifecycle/livedata/src/test/java/androidx/lifecycle/TransformationsTest.java b/lifecycle/livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
new file mode 100644
index 0000000..4994523
--- /dev/null
+++ b/lifecycle/livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.arch.core.util.Function;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.lifecycle.util.InstantTaskExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+public class TransformationsTest {
+
+ private LifecycleOwner mOwner;
+
+ @Before
+ public void swapExecutorDelegate() {
+ ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
+ }
+
+ @Before
+ public void setup() {
+ mOwner = mock(LifecycleOwner.class);
+ LifecycleRegistry registry = new LifecycleRegistry(mOwner);
+ when(mOwner.getLifecycle()).thenReturn(registry);
+ registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ registry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ }
+
+ @Test
+ public void testMap() {
+ LiveData<String> source = new MutableLiveData<>();
+ LiveData<Integer> mapped = Transformations.map(source, new Function<String, Integer>() {
+ @Override
+ public Integer apply(String input) {
+ return input.length();
+ }
+ });
+ Observer<Integer> observer = mock(Observer.class);
+ mapped.observe(mOwner, observer);
+ source.setValue("four");
+ verify(observer).onChanged(4);
+ }
+
+ @Test
+ public void testSwitchMap() {
+ LiveData<Integer> trigger = new MutableLiveData<>();
+ final LiveData<String> first = new MutableLiveData<>();
+ final LiveData<String> second = new MutableLiveData<>();
+ LiveData<String> result = Transformations.switchMap(trigger,
+ new Function<Integer, LiveData<String>>() {
+ @Override
+ public LiveData<String> apply(Integer input) {
+ if (input == 1) {
+ return first;
+ } else {
+ return second;
+ }
+ }
+ });
+
+ Observer<String> observer = mock(Observer.class);
+ result.observe(mOwner, observer);
+ verify(observer, never()).onChanged(anyString());
+ first.setValue("first");
+ trigger.setValue(1);
+ verify(observer).onChanged("first");
+ second.setValue("second");
+ reset(observer);
+ verify(observer, never()).onChanged(anyString());
+ trigger.setValue(2);
+ verify(observer).onChanged("second");
+ reset(observer);
+ first.setValue("failure");
+ verify(observer, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void testSwitchMap2() {
+ LiveData<Integer> trigger = new MutableLiveData<>();
+ final LiveData<String> first = new MutableLiveData<>();
+ final LiveData<String> second = new MutableLiveData<>();
+ LiveData<String> result = Transformations.switchMap(trigger,
+ new Function<Integer, LiveData<String>>() {
+ @Override
+ public LiveData<String> apply(Integer input) {
+ if (input == 1) {
+ return first;
+ } else {
+ return second;
+ }
+ }
+ });
+
+ Observer<String> observer = mock(Observer.class);
+ result.observe(mOwner, observer);
+
+ verify(observer, never()).onChanged(anyString());
+ trigger.setValue(1);
+ verify(observer, never()).onChanged(anyString());
+ first.setValue("fi");
+ verify(observer).onChanged("fi");
+ first.setValue("rst");
+ verify(observer).onChanged("rst");
+
+ second.setValue("second");
+ reset(observer);
+ verify(observer, never()).onChanged(anyString());
+ trigger.setValue(2);
+ verify(observer).onChanged("second");
+ reset(observer);
+ first.setValue("failure");
+ verify(observer, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void testNoRedispatchSwitchMap() {
+ LiveData<Integer> trigger = new MutableLiveData<>();
+ final LiveData<String> first = new MutableLiveData<>();
+ LiveData<String> result = Transformations.switchMap(trigger,
+ new Function<Integer, LiveData<String>>() {
+ @Override
+ public LiveData<String> apply(Integer input) {
+ return first;
+ }
+ });
+
+ Observer<String> observer = mock(Observer.class);
+ result.observe(mOwner, observer);
+ verify(observer, never()).onChanged(anyString());
+ first.setValue("first");
+ trigger.setValue(1);
+ verify(observer).onChanged("first");
+ reset(observer);
+ trigger.setValue(2);
+ verify(observer, never()).onChanged(anyString());
+ }
+
+ @Test
+ public void testSwitchMapToNull() {
+ LiveData<Integer> trigger = new MutableLiveData<>();
+ final LiveData<String> first = new MutableLiveData<>();
+ LiveData<String> result = Transformations.switchMap(trigger,
+ new Function<Integer, LiveData<String>>() {
+ @Override
+ public LiveData<String> apply(Integer input) {
+ if (input == 1) {
+ return first;
+ } else {
+ return null;
+ }
+ }
+ });
+
+ Observer<String> observer = mock(Observer.class);
+ result.observe(mOwner, observer);
+ verify(observer, never()).onChanged(anyString());
+ first.setValue("first");
+ trigger.setValue(1);
+ verify(observer).onChanged("first");
+ reset(observer);
+
+ trigger.setValue(2);
+ verify(observer, never()).onChanged(anyString());
+ assertThat(first.hasObservers(), is(false));
+ }
+
+ @Test
+ public void noObsoleteValueTest() {
+ MutableLiveData<Integer> numbers = new MutableLiveData<>();
+ LiveData<Integer> squared = Transformations.map(numbers, new Function<Integer, Integer>() {
+ @Override
+ public Integer apply(Integer input) {
+ return input * input;
+ }
+ });
+
+ Observer observer = mock(Observer.class);
+ squared.setValue(1);
+ squared.observeForever(observer);
+ verify(observer).onChanged(1);
+ squared.removeObserver(observer);
+ reset(observer);
+ numbers.setValue(2);
+ squared.observeForever(observer);
+ verify(observer, only()).onChanged(4);
+ }
+}
diff --git a/lifecycle/livedata/src/test/java/androidx/lifecycle/util/InstantTaskExecutor.java b/lifecycle/livedata/src/test/java/androidx/lifecycle/util/InstantTaskExecutor.java
new file mode 100644
index 0000000..60d703f
--- /dev/null
+++ b/lifecycle/livedata/src/test/java/androidx/lifecycle/util/InstantTaskExecutor.java
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.lifecycle.util;
+
+import androidx.arch.core.executor.TaskExecutor;
+
+public class InstantTaskExecutor extends TaskExecutor {
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return true;
+ }
+}
diff --git a/lifecycle/reactivestreams/api/current.txt b/lifecycle/reactivestreams/api/current.txt
index 4a509e9..d342cca 100644
--- a/lifecycle/reactivestreams/api/current.txt
+++ b/lifecycle/reactivestreams/api/current.txt
@@ -1,8 +1,8 @@
-package android.arch.lifecycle {
+package androidx.lifecycle {
public final class LiveDataReactiveStreams {
- method public static <T> android.arch.lifecycle.LiveData<T> fromPublisher(org.reactivestreams.Publisher<T>);
- method public static <T> org.reactivestreams.Publisher<T> toPublisher(android.arch.lifecycle.LifecycleOwner, android.arch.lifecycle.LiveData<T>);
+ method public static <T> androidx.lifecycle.LiveData<T> fromPublisher(org.reactivestreams.Publisher<T>);
+ method public static <T> org.reactivestreams.Publisher<T> toPublisher(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.LiveData<T>);
}
}
diff --git a/lifecycle/reactivestreams/api/1.0.0.txt b/lifecycle/reactivestreams/api_legacy/1.0.0.txt
similarity index 100%
rename from lifecycle/reactivestreams/api/1.0.0.txt
rename to lifecycle/reactivestreams/api_legacy/1.0.0.txt
diff --git a/lifecycle/reactivestreams/api/1.1.0.txt b/lifecycle/reactivestreams/api_legacy/1.1.0.txt
similarity index 100%
rename from lifecycle/reactivestreams/api/1.1.0.txt
rename to lifecycle/reactivestreams/api_legacy/1.1.0.txt
diff --git a/lifecycle/reactivestreams/api/1.0.0.txt b/lifecycle/reactivestreams/api_legacy/current.txt
similarity index 100%
copy from lifecycle/reactivestreams/api/1.0.0.txt
copy to lifecycle/reactivestreams/api_legacy/current.txt
diff --git a/lifecycle/reactivestreams/build.gradle b/lifecycle/reactivestreams/build.gradle
index 12f12b1..34390b7 100644
--- a/lifecycle/reactivestreams/build.gradle
+++ b/lifecycle/reactivestreams/build.gradle
@@ -24,10 +24,10 @@
}
dependencies {
- api(project(":arch:common"))
- api(project(":lifecycle:common"))
- api(project(":lifecycle:livedata"))
- api(project(":lifecycle:runtime"))
+ api(project(":arch:core-common"))
+ api(project(":lifecycle:lifecycle-common"))
+ api(project(":lifecycle:lifecycle-livedata"))
+ api(project(":lifecycle:lifecycle-runtime"))
api(SUPPORT_ANNOTATIONS)
api(REACTIVE_STREAMS)
diff --git a/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml b/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
index e60c9f4..304fd93 100644
--- a/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
+++ b/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
@@ -16,11 +16,11 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.reactivestreams.test">
+ package="androidx.lifecycle.reactivestreams.test">
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<application>
- <activity android:name="android.arch.lifecycle.viewmodeltest.ViewModelActivity"
+ <activity android:name="androidx.lifecycle.viewmodeltest.ViewModelActivity"
android:theme="@style/Base.Theme.AppCompat">
</activity>
</application>
diff --git a/lifecycle/reactivestreams/src/main/AndroidManifest.xml b/lifecycle/reactivestreams/src/main/AndroidManifest.xml
index 2210041..96fed76 100644
--- a/lifecycle/reactivestreams/src/main/AndroidManifest.xml
+++ b/lifecycle/reactivestreams/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.reactivestreams">
+ package="androidx.lifecycle.reactivestreams">
</manifest>
diff --git a/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java b/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java
deleted file mode 100644
index ed3c57c..0000000
--- a/lifecycle/reactivestreams/src/main/java/android/arch/lifecycle/LiveDataReactiveStreams.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * 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.arch.core.executor.ArchTaskExecutor;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import org.reactivestreams.Publisher;
-import org.reactivestreams.Subscriber;
-import org.reactivestreams.Subscription;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Adapts {@link LiveData} input and output to the ReactiveStreams spec.
- */
-@SuppressWarnings("WeakerAccess")
-public final class LiveDataReactiveStreams {
- private LiveDataReactiveStreams() {
- }
-
- /**
- * Adapts the given {@link LiveData} stream to a ReactiveStreams {@link Publisher}.
- *
- * <p>
- * By using a good publisher implementation such as RxJava 2.x Flowables, most consumers will
- * be able to let the library deal with backpressure using operators and not need to worry about
- * ever manually calling {@link Subscription#request}.
- *
- * <p>
- * On subscription to the publisher, the observer will attach to the given {@link LiveData}.
- * Once {@link Subscription#request) is called on the subscription object, an observer will be
- * connected to the data stream. Calling request(Long.MAX_VALUE) is equivalent to creating an
- * unbounded stream with no backpressure. If request with a finite count reaches 0, the observer
- * will buffer the latest item and emit it to the subscriber when data is again requested. Any
- * other items emitted during the time there was no backpressure requested will be dropped.
- */
- @NonNull
- public static <T> Publisher<T> toPublisher(
- @NonNull LifecycleOwner lifecycle, @NonNull LiveData<T> liveData) {
-
- return new LiveDataPublisher<>(lifecycle, liveData);
- }
-
- private static final class LiveDataPublisher<T> implements Publisher<T> {
- final LifecycleOwner mLifecycle;
- final LiveData<T> mLiveData;
-
- LiveDataPublisher(LifecycleOwner lifecycle, LiveData<T> liveData) {
- this.mLifecycle = lifecycle;
- this.mLiveData = liveData;
- }
-
- @Override
- public void subscribe(Subscriber<? super T> subscriber) {
- subscriber.onSubscribe(new LiveDataSubscription<T>(subscriber, mLifecycle, mLiveData));
- }
-
- static final class LiveDataSubscription<T> implements Subscription, Observer<T> {
- final Subscriber<? super T> mSubscriber;
- final LifecycleOwner mLifecycle;
- final LiveData<T> mLiveData;
-
- volatile boolean mCanceled;
- // used on main thread only
- boolean mObserving;
- long mRequested;
- // used on main thread only
- @Nullable
- T mLatest;
-
- LiveDataSubscription(final Subscriber<? super T> subscriber,
- final LifecycleOwner lifecycle, final LiveData<T> liveData) {
- this.mSubscriber = subscriber;
- this.mLifecycle = lifecycle;
- this.mLiveData = liveData;
- }
-
- @Override
- public void onChanged(@Nullable T t) {
- if (mCanceled) {
- return;
- }
- if (mRequested > 0) {
- mLatest = null;
- mSubscriber.onNext(t);
- if (mRequested != Long.MAX_VALUE) {
- mRequested--;
- }
- } else {
- mLatest = t;
- }
- }
-
- @Override
- public void request(final long n) {
- if (mCanceled) {
- return;
- }
- ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
- @Override
- public void run() {
- if (mCanceled) {
- return;
- }
- if (n <= 0L) {
- mCanceled = true;
- if (mObserving) {
- mLiveData.removeObserver(LiveDataSubscription.this);
- mObserving = false;
- }
- mLatest = null;
- mSubscriber.onError(
- new IllegalArgumentException("Non-positive request"));
- return;
- }
-
- // Prevent overflowage.
- mRequested = mRequested + n >= mRequested
- ? mRequested + n : Long.MAX_VALUE;
- if (!mObserving) {
- mObserving = true;
- mLiveData.observe(mLifecycle, LiveDataSubscription.this);
- } else if (mLatest != null) {
- onChanged(mLatest);
- mLatest = null;
- }
- }
- });
- }
-
- @Override
- public void cancel() {
- if (mCanceled) {
- return;
- }
- mCanceled = true;
- ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
- @Override
- public void run() {
- if (mObserving) {
- mLiveData.removeObserver(LiveDataSubscription.this);
- mObserving = false;
- }
- mLatest = null;
- }
- });
- }
- }
- }
-
- /**
- * 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.
- * <p>
- * Note that LiveData does NOT handle errors and it expects that errors are treated as states
- * in the data that's held. In case of an error being emitted by the publisher, an error will
- * be propagated to the main thread and the app will crash.
- *
- * @param <T> The type of data hold by this instance.
- */
- @NonNull
- public static <T> LiveData<T> fromPublisher(@NonNull Publisher<T> publisher) {
- 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.
- *
- * <p>
- * Note that LiveData does NOT handle errors and it expects that errors are treated as states
- * in the data that's held. In case of an error being emitted by the publisher, an error will
- * be propagated to the main thread and the app will crash.
- *
- * @param <T> The type of data hold by this instance.
- */
- private static class PublisherLiveData<T> extends LiveData<T> {
- private final Publisher<T> mPublisher;
- final AtomicReference<LiveDataSubscriber> mSubscriber;
-
- PublisherLiveData(@NonNull Publisher<T> publisher) {
- mPublisher = publisher;
- mSubscriber = new AtomicReference<>();
- }
-
- @Override
- protected void onActive() {
- super.onActive();
- LiveDataSubscriber s = new LiveDataSubscriber();
- mSubscriber.set(s);
- mPublisher.subscribe(s);
- }
-
- @Override
- protected void onInactive() {
- super.onInactive();
- LiveDataSubscriber s = mSubscriber.getAndSet(null);
- if (s != null) {
- s.cancelSubscription();
- }
- }
-
- final class LiveDataSubscriber extends AtomicReference<Subscription>
- implements Subscriber<T> {
-
- @Override
- public void onSubscribe(Subscription s) {
- if (compareAndSet(null, s)) {
- s.request(Long.MAX_VALUE);
- } else {
- s.cancel();
- }
- }
-
- @Override
- public void onNext(T item) {
- postValue(item);
- }
-
- @Override
- public void onError(final Throwable ex) {
- mSubscriber.compareAndSet(this, null);
-
- ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
- @Override
- public void run() {
- // Errors should be handled upstream, so propagate as a crash.
- throw new RuntimeException("LiveData does not handle errors. Errors from "
- + "publishers should be handled upstream and propagated as "
- + "state", ex);
- }
- });
- }
-
- @Override
- public void onComplete() {
- mSubscriber.compareAndSet(this, null);
- }
-
- public void cancelSubscription() {
- Subscription s = get();
- if (s != null) {
- s.cancel();
- }
- }
- }
- }
-}
diff --git a/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java b/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
new file mode 100644
index 0000000..4af5a09
--- /dev/null
+++ b/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
@@ -0,0 +1,284 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Adapts {@link LiveData} input and output to the ReactiveStreams spec.
+ */
+@SuppressWarnings("WeakerAccess")
+public final class LiveDataReactiveStreams {
+ private LiveDataReactiveStreams() {
+ }
+
+ /**
+ * Adapts the given {@link LiveData} stream to a ReactiveStreams {@link Publisher}.
+ *
+ * <p>
+ * By using a good publisher implementation such as RxJava 2.x Flowables, most consumers will
+ * be able to let the library deal with backpressure using operators and not need to worry about
+ * ever manually calling {@link Subscription#request}.
+ *
+ * <p>
+ * On subscription to the publisher, the observer will attach to the given {@link LiveData}.
+ * Once {@link Subscription#request) is called on the subscription object, an observer will be
+ * connected to the data stream. Calling request(Long.MAX_VALUE) is equivalent to creating an
+ * unbounded stream with no backpressure. If request with a finite count reaches 0, the observer
+ * will buffer the latest item and emit it to the subscriber when data is again requested. Any
+ * other items emitted during the time there was no backpressure requested will be dropped.
+ */
+ @NonNull
+ public static <T> Publisher<T> toPublisher(
+ @NonNull LifecycleOwner lifecycle, @NonNull LiveData<T> liveData) {
+
+ return new LiveDataPublisher<>(lifecycle, liveData);
+ }
+
+ private static final class LiveDataPublisher<T> implements Publisher<T> {
+ final LifecycleOwner mLifecycle;
+ final LiveData<T> mLiveData;
+
+ LiveDataPublisher(LifecycleOwner lifecycle, LiveData<T> liveData) {
+ this.mLifecycle = lifecycle;
+ this.mLiveData = liveData;
+ }
+
+ @Override
+ public void subscribe(Subscriber<? super T> subscriber) {
+ subscriber.onSubscribe(new LiveDataSubscription<T>(subscriber, mLifecycle, mLiveData));
+ }
+
+ static final class LiveDataSubscription<T> implements Subscription, Observer<T> {
+ final Subscriber<? super T> mSubscriber;
+ final LifecycleOwner mLifecycle;
+ final LiveData<T> mLiveData;
+
+ volatile boolean mCanceled;
+ // used on main thread only
+ boolean mObserving;
+ long mRequested;
+ // used on main thread only
+ @Nullable
+ T mLatest;
+
+ LiveDataSubscription(final Subscriber<? super T> subscriber,
+ final LifecycleOwner lifecycle, final LiveData<T> liveData) {
+ this.mSubscriber = subscriber;
+ this.mLifecycle = lifecycle;
+ this.mLiveData = liveData;
+ }
+
+ @Override
+ public void onChanged(@Nullable T t) {
+ if (mCanceled) {
+ return;
+ }
+ if (mRequested > 0) {
+ mLatest = null;
+ mSubscriber.onNext(t);
+ if (mRequested != Long.MAX_VALUE) {
+ mRequested--;
+ }
+ } else {
+ mLatest = t;
+ }
+ }
+
+ @Override
+ public void request(final long n) {
+ if (mCanceled) {
+ return;
+ }
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mCanceled) {
+ return;
+ }
+ if (n <= 0L) {
+ mCanceled = true;
+ if (mObserving) {
+ mLiveData.removeObserver(LiveDataSubscription.this);
+ mObserving = false;
+ }
+ mLatest = null;
+ mSubscriber.onError(
+ new IllegalArgumentException("Non-positive request"));
+ return;
+ }
+
+ // Prevent overflowage.
+ mRequested = mRequested + n >= mRequested
+ ? mRequested + n : Long.MAX_VALUE;
+ if (!mObserving) {
+ mObserving = true;
+ mLiveData.observe(mLifecycle, LiveDataSubscription.this);
+ } else if (mLatest != null) {
+ onChanged(mLatest);
+ mLatest = null;
+ }
+ }
+ });
+ }
+
+ @Override
+ public void cancel() {
+ if (mCanceled) {
+ return;
+ }
+ mCanceled = true;
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mObserving) {
+ mLiveData.removeObserver(LiveDataSubscription.this);
+ mObserving = false;
+ }
+ mLatest = null;
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * Note that LiveData does NOT handle errors and it expects that errors are treated as states
+ * in the data that's held. In case of an error being emitted by the publisher, an error will
+ * be propagated to the main thread and the app will crash.
+ *
+ * @param <T> The type of data hold by this instance.
+ */
+ @NonNull
+ public static <T> LiveData<T> fromPublisher(@NonNull Publisher<T> publisher) {
+ 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.
+ *
+ * <p>
+ * Note that LiveData does NOT handle errors and it expects that errors are treated as states
+ * in the data that's held. In case of an error being emitted by the publisher, an error will
+ * be propagated to the main thread and the app will crash.
+ *
+ * @param <T> The type of data hold by this instance.
+ */
+ private static class PublisherLiveData<T> extends LiveData<T> {
+ private final Publisher<T> mPublisher;
+ final AtomicReference<LiveDataSubscriber> mSubscriber;
+
+ PublisherLiveData(@NonNull Publisher<T> publisher) {
+ mPublisher = publisher;
+ mSubscriber = new AtomicReference<>();
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+ LiveDataSubscriber s = new LiveDataSubscriber();
+ mSubscriber.set(s);
+ mPublisher.subscribe(s);
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ LiveDataSubscriber s = mSubscriber.getAndSet(null);
+ if (s != null) {
+ s.cancelSubscription();
+ }
+ }
+
+ final class LiveDataSubscriber extends AtomicReference<Subscription>
+ implements Subscriber<T> {
+
+ @Override
+ public void onSubscribe(Subscription s) {
+ if (compareAndSet(null, s)) {
+ s.request(Long.MAX_VALUE);
+ } else {
+ s.cancel();
+ }
+ }
+
+ @Override
+ public void onNext(T item) {
+ postValue(item);
+ }
+
+ @Override
+ public void onError(final Throwable ex) {
+ mSubscriber.compareAndSet(this, null);
+
+ ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ // Errors should be handled upstream, so propagate as a crash.
+ throw new RuntimeException("LiveData does not handle errors. Errors from "
+ + "publishers should be handled upstream and propagated as "
+ + "state", ex);
+ }
+ });
+ }
+
+ @Override
+ public void onComplete() {
+ mSubscriber.compareAndSet(this, null);
+ }
+
+ public void cancelSubscription() {
+ Subscription s = get();
+ if (s != null) {
+ s.cancel();
+ }
+ }
+ }
+ }
+}
diff --git a/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
deleted file mode 100644
index 163cff0..0000000
--- a/lifecycle/reactivestreams/src/test/java/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
+++ /dev/null
@@ -1,381 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.TaskExecutor;
-import android.support.annotation.Nullable;
-import android.support.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.reactivestreams.Subscriber;
-import org.reactivestreams.Subscription;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import io.reactivex.Flowable;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.functions.Consumer;
-import io.reactivex.processors.PublishProcessor;
-import io.reactivex.processors.ReplayProcessor;
-import io.reactivex.schedulers.TestScheduler;
-import io.reactivex.subjects.AsyncSubject;
-
-@SmallTest
-public class LiveDataReactiveStreamsTest {
- private LifecycleOwner mLifecycleOwner;
-
- private final List<String> mLiveDataOutput = new ArrayList<>();
- private final Observer<String> mObserver = new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- mLiveDataOutput.add(s);
- }
- };
-
- private final ReplayProcessor<String> mOutputProcessor = ReplayProcessor.create();
-
- private static final TestScheduler sBackgroundScheduler = new TestScheduler();
- private Thread mTestThread;
-
- @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();
- ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
-
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- throw new IllegalStateException();
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- // Wrong implementation, but it is fine for test
- runnable.run();
- }
-
- @Override
- public boolean isMainThread() {
- return Thread.currentThread() == mTestThread;
- }
-
- });
- }
-
- @After
- public void removeExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-
- @Test
- public void convertsFromPublisher() {
- PublishProcessor<String> processor = PublishProcessor.create();
- LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
-
- liveData.observe(mLifecycleOwner, mObserver);
-
- processor.onNext("foo");
- processor.onNext("bar");
- processor.onNext("baz");
-
- assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
- }
-
- @Test
- public void convertsFromPublisherSubscribeWithDelay() {
- PublishProcessor<String> processor = PublishProcessor.create();
- processor.delaySubscription(100, TimeUnit.SECONDS, sBackgroundScheduler);
- LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
-
- liveData.observe(mLifecycleOwner, mObserver);
-
- processor.onNext("foo");
- liveData.removeObserver(mObserver);
- sBackgroundScheduler.triggerActions();
- liveData.observe(mLifecycleOwner, mObserver);
-
- processor.onNext("bar");
- processor.onNext("baz");
-
- assertThat(mLiveDataOutput, is(Arrays.asList("foo", "foo", "bar", "baz")));
- }
-
- @Test
- public void convertsFromPublisherThrowsException() {
- PublishProcessor<String> processor = PublishProcessor.create();
- LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
-
- liveData.observe(mLifecycleOwner, mObserver);
-
- IllegalStateException exception = new IllegalStateException("test exception");
- try {
- processor.onError(exception);
- fail("Runtime Exception expected");
- } catch (RuntimeException ex) {
- assertEquals(ex.getCause(), exception);
- }
- }
-
- @Test
- public void convertsFromPublisherWithMultipleObservers() {
- final List<String> output2 = new ArrayList<>();
- PublishProcessor<String> processor = PublishProcessor.create();
- LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
-
- liveData.observe(mLifecycleOwner, mObserver);
-
- processor.onNext("foo");
- processor.onNext("bar");
-
- // The second observer should only get the newest value and any later values.
- liveData.observe(mLifecycleOwner, new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- output2.add(s);
- }
- });
-
- processor.onNext("baz");
-
- assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
- assertThat(output2, is(Arrays.asList("bar", "baz")));
- }
-
- @Test
- public void convertsFromPublisherWithMultipleObserversAfterInactive() {
- final List<String> output2 = new ArrayList<>();
- PublishProcessor<String> processor = PublishProcessor.create();
- LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
-
- liveData.observe(mLifecycleOwner, mObserver);
-
- processor.onNext("foo");
- processor.onNext("bar");
-
- // The second observer should only get the newest value and any later values.
- liveData.observe(mLifecycleOwner, new Observer<String>() {
- @Override
- public void onChanged(@Nullable String s) {
- output2.add(s);
- }
- });
-
- liveData.removeObserver(mObserver);
- processor.onNext("baz");
-
- assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar")));
- assertThat(output2, is(Arrays.asList("bar", "baz")));
- }
-
- @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 convertsFromPublisherManagesSubscriptions() {
- 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(mLifecycleOwner, mObserver);
-
- assertThat(mLiveDataOutput, is(Collections.singletonList("foo")));
- sBackgroundScheduler.triggerActions();
- assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
- }
-
- @Test
- public void convertsToPublisherWithSyncData() {
- MutableLiveData<String> liveData = new MutableLiveData<>();
- liveData.setValue("foo");
- assertThat(liveData.getValue(), is("foo"));
-
- Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
- .subscribe(mOutputProcessor);
-
- liveData.setValue("bar");
- liveData.setValue("baz");
-
- assertThat(
- mOutputProcessor.getValues(new String[]{}),
- is(new String[]{"foo", "bar", "baz"}));
- }
-
- @Test
- public void convertingToPublisherIsCancelable() {
- MutableLiveData<String> liveData = new MutableLiveData<>();
- liveData.setValue("foo");
- assertThat(liveData.getValue(), is("foo"));
-
- Disposable disposable = Flowable
- .fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
- .subscribe(new Consumer<String>() {
- @Override
- public void accept(String s) throws Exception {
- mLiveDataOutput.add(s);
- }
- });
-
- liveData.setValue("bar");
- liveData.setValue("baz");
-
- assertThat(liveData.hasObservers(), is(true));
- disposable.dispose();
-
- liveData.setValue("fizz");
- liveData.setValue("buzz");
-
- assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
- // Canceling disposable should also remove livedata mObserver.
- assertThat(liveData.hasObservers(), is(false));
- }
-
- @Test
- public void convertsToPublisherWithBackpressure() {
- MutableLiveData<String> liveData = new MutableLiveData<>();
-
- final AsyncSubject<Subscription> subscriptionSubject = AsyncSubject.create();
-
- Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
- .subscribe(new Subscriber<String>() {
- @Override
- public void onSubscribe(Subscription s) {
- subscriptionSubject.onNext(s);
- subscriptionSubject.onComplete();
- }
-
- @Override
- public void onNext(String s) {
- mOutputProcessor.onNext(s);
- }
-
- @Override
- public void onError(Throwable t) {
- throw new RuntimeException(t);
- }
-
- @Override
- public void onComplete() {
- }
- });
-
- // Subscription should have happened synchronously. If it didn't, this will deadlock.
- final Subscription subscription = subscriptionSubject.blockingSingle();
-
- subscription.request(1);
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{}));
-
- liveData.setValue("foo");
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
-
- subscription.request(2);
- liveData.setValue("baz");
- liveData.setValue("fizz");
-
- assertThat(
- mOutputProcessor.getValues(new String[]{}),
- is(new String[]{"foo", "baz", "fizz"}));
-
- // 'nyan' will be dropped as there is nothing currently requesting a stream.
- liveData.setValue("nyan");
- liveData.setValue("cat");
-
- assertThat(
- mOutputProcessor.getValues(new String[]{}),
- is(new String[]{"foo", "baz", "fizz"}));
-
- // When a new request comes in, the latest value will be pushed.
- subscription.request(1);
- assertThat(
- mOutputProcessor.getValues(new String[]{}),
- is(new String[]{"foo", "baz", "fizz", "cat"}));
- }
-
- @Test
- public void convertsToPublisherWithAsyncData() {
- MutableLiveData<String> liveData = new MutableLiveData<>();
-
- Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
- .observeOn(sBackgroundScheduler)
- .subscribe(mOutputProcessor);
-
- liveData.setValue("foo");
-
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{}));
- sBackgroundScheduler.triggerActions();
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
-
- liveData.setValue("bar");
- liveData.setValue("baz");
-
- assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
- sBackgroundScheduler.triggerActions();
- assertThat(mOutputProcessor.getValues(
- new String[]{}),
- is(new String[]{"foo", "bar", "baz"}));
- }
-}
diff --git a/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java b/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
new file mode 100644
index 0000000..6311d94
--- /dev/null
+++ b/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Flowable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Consumer;
+import io.reactivex.processors.PublishProcessor;
+import io.reactivex.processors.ReplayProcessor;
+import io.reactivex.schedulers.TestScheduler;
+import io.reactivex.subjects.AsyncSubject;
+
+@SmallTest
+public class LiveDataReactiveStreamsTest {
+ private LifecycleOwner mLifecycleOwner;
+
+ private final List<String> mLiveDataOutput = new ArrayList<>();
+ private final Observer<String> mObserver = new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveDataOutput.add(s);
+ }
+ };
+
+ private final ReplayProcessor<String> mOutputProcessor = ReplayProcessor.create();
+
+ private static final TestScheduler sBackgroundScheduler = new TestScheduler();
+ private Thread mTestThread;
+
+ @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();
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ // Wrong implementation, but it is fine for test
+ runnable.run();
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return Thread.currentThread() == mTestThread;
+ }
+
+ });
+ }
+
+ @After
+ public void removeExecutorDelegate() {
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+
+ @Test
+ public void convertsFromPublisher() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("foo");
+ processor.onNext("bar");
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
+ }
+
+ @Test
+ public void convertsFromPublisherSubscribeWithDelay() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ processor.delaySubscription(100, TimeUnit.SECONDS, sBackgroundScheduler);
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("foo");
+ liveData.removeObserver(mObserver);
+ sBackgroundScheduler.triggerActions();
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("bar");
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "foo", "bar", "baz")));
+ }
+
+ @Test
+ public void convertsFromPublisherThrowsException() {
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ IllegalStateException exception = new IllegalStateException("test exception");
+ try {
+ processor.onError(exception);
+ fail("Runtime Exception expected");
+ } catch (RuntimeException ex) {
+ assertEquals(ex.getCause(), exception);
+ }
+ }
+
+ @Test
+ public void convertsFromPublisherWithMultipleObservers() {
+ final List<String> output2 = new ArrayList<>();
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("foo");
+ processor.onNext("bar");
+
+ // The second observer should only get the newest value and any later values.
+ liveData.observe(mLifecycleOwner, new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ output2.add(s);
+ }
+ });
+
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
+ assertThat(output2, is(Arrays.asList("bar", "baz")));
+ }
+
+ @Test
+ public void convertsFromPublisherWithMultipleObserversAfterInactive() {
+ final List<String> output2 = new ArrayList<>();
+ PublishProcessor<String> processor = PublishProcessor.create();
+ LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor);
+
+ liveData.observe(mLifecycleOwner, mObserver);
+
+ processor.onNext("foo");
+ processor.onNext("bar");
+
+ // The second observer should only get the newest value and any later values.
+ liveData.observe(mLifecycleOwner, new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ output2.add(s);
+ }
+ });
+
+ liveData.removeObserver(mObserver);
+ processor.onNext("baz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar")));
+ assertThat(output2, is(Arrays.asList("bar", "baz")));
+ }
+
+ @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 convertsFromPublisherManagesSubscriptions() {
+ 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(mLifecycleOwner, mObserver);
+
+ assertThat(mLiveDataOutput, is(Collections.singletonList("foo")));
+ sBackgroundScheduler.triggerActions();
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
+ }
+
+ @Test
+ public void convertsToPublisherWithSyncData() {
+ MutableLiveData<String> liveData = new MutableLiveData<>();
+ liveData.setValue("foo");
+ assertThat(liveData.getValue(), is("foo"));
+
+ Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
+ .subscribe(mOutputProcessor);
+
+ liveData.setValue("bar");
+ liveData.setValue("baz");
+
+ assertThat(
+ mOutputProcessor.getValues(new String[]{}),
+ is(new String[]{"foo", "bar", "baz"}));
+ }
+
+ @Test
+ public void convertingToPublisherIsCancelable() {
+ MutableLiveData<String> liveData = new MutableLiveData<>();
+ liveData.setValue("foo");
+ assertThat(liveData.getValue(), is("foo"));
+
+ Disposable disposable = Flowable
+ .fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
+ .subscribe(new Consumer<String>() {
+ @Override
+ public void accept(String s) throws Exception {
+ mLiveDataOutput.add(s);
+ }
+ });
+
+ liveData.setValue("bar");
+ liveData.setValue("baz");
+
+ assertThat(liveData.hasObservers(), is(true));
+ disposable.dispose();
+
+ liveData.setValue("fizz");
+ liveData.setValue("buzz");
+
+ assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar", "baz")));
+ // Canceling disposable should also remove livedata mObserver.
+ assertThat(liveData.hasObservers(), is(false));
+ }
+
+ @Test
+ public void convertsToPublisherWithBackpressure() {
+ MutableLiveData<String> liveData = new MutableLiveData<>();
+
+ final AsyncSubject<Subscription> subscriptionSubject = AsyncSubject.create();
+
+ Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
+ .subscribe(new Subscriber<String>() {
+ @Override
+ public void onSubscribe(Subscription s) {
+ subscriptionSubject.onNext(s);
+ subscriptionSubject.onComplete();
+ }
+
+ @Override
+ public void onNext(String s) {
+ mOutputProcessor.onNext(s);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ throw new RuntimeException(t);
+ }
+
+ @Override
+ public void onComplete() {
+ }
+ });
+
+ // Subscription should have happened synchronously. If it didn't, this will deadlock.
+ final Subscription subscription = subscriptionSubject.blockingSingle();
+
+ subscription.request(1);
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{}));
+
+ liveData.setValue("foo");
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
+
+ subscription.request(2);
+ liveData.setValue("baz");
+ liveData.setValue("fizz");
+
+ assertThat(
+ mOutputProcessor.getValues(new String[]{}),
+ is(new String[]{"foo", "baz", "fizz"}));
+
+ // 'nyan' will be dropped as there is nothing currently requesting a stream.
+ liveData.setValue("nyan");
+ liveData.setValue("cat");
+
+ assertThat(
+ mOutputProcessor.getValues(new String[]{}),
+ is(new String[]{"foo", "baz", "fizz"}));
+
+ // When a new request comes in, the latest value will be pushed.
+ subscription.request(1);
+ assertThat(
+ mOutputProcessor.getValues(new String[]{}),
+ is(new String[]{"foo", "baz", "fizz", "cat"}));
+ }
+
+ @Test
+ public void convertsToPublisherWithAsyncData() {
+ MutableLiveData<String> liveData = new MutableLiveData<>();
+
+ Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData))
+ .observeOn(sBackgroundScheduler)
+ .subscribe(mOutputProcessor);
+
+ liveData.setValue("foo");
+
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{}));
+ sBackgroundScheduler.triggerActions();
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
+
+ liveData.setValue("bar");
+ liveData.setValue("baz");
+
+ assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"}));
+ sBackgroundScheduler.triggerActions();
+ assertThat(mOutputProcessor.getValues(
+ new String[]{}),
+ is(new String[]{"foo", "bar", "baz"}));
+ }
+}
diff --git a/lifecycle/runtime/api/current.txt b/lifecycle/runtime/api/current.txt
index 2b900b0..84da3d1 100644
--- a/lifecycle/runtime/api/current.txt
+++ b/lifecycle/runtime/api/current.txt
@@ -1,17 +1,17 @@
-package android.arch.lifecycle {
+package androidx.lifecycle {
- public class LifecycleRegistry extends android.arch.lifecycle.Lifecycle {
- ctor public LifecycleRegistry(android.arch.lifecycle.LifecycleOwner);
- method public void addObserver(android.arch.lifecycle.LifecycleObserver);
- method public android.arch.lifecycle.Lifecycle.State getCurrentState();
+ public class LifecycleRegistry extends androidx.lifecycle.Lifecycle {
+ ctor public LifecycleRegistry(androidx.lifecycle.LifecycleOwner);
+ method public void addObserver(androidx.lifecycle.LifecycleObserver);
+ method public androidx.lifecycle.Lifecycle.State getCurrentState();
method public int getObserverCount();
- method public void handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event);
- method public void markState(android.arch.lifecycle.Lifecycle.State);
- method public void removeObserver(android.arch.lifecycle.LifecycleObserver);
+ method public void handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event);
+ method public void markState(androidx.lifecycle.Lifecycle.State);
+ method public void removeObserver(androidx.lifecycle.LifecycleObserver);
}
- public abstract deprecated interface LifecycleRegistryOwner implements android.arch.lifecycle.LifecycleOwner {
- method public abstract android.arch.lifecycle.LifecycleRegistry getLifecycle();
+ public abstract deprecated interface LifecycleRegistryOwner implements androidx.lifecycle.LifecycleOwner {
+ method public abstract androidx.lifecycle.LifecycleRegistry getLifecycle();
}
}
diff --git a/lifecycle/runtime/api/0.0.0.txt b/lifecycle/runtime/api_legacy/0.0.0.txt
similarity index 100%
rename from lifecycle/runtime/api/0.0.0.txt
rename to lifecycle/runtime/api_legacy/0.0.0.txt
diff --git a/lifecycle/runtime/api/1.0.0.txt b/lifecycle/runtime/api_legacy/1.0.0.txt
similarity index 100%
rename from lifecycle/runtime/api/1.0.0.txt
rename to lifecycle/runtime/api_legacy/1.0.0.txt
diff --git a/lifecycle/runtime/api/1.1.0.txt b/lifecycle/runtime/api_legacy/1.1.0.txt
similarity index 100%
rename from lifecycle/runtime/api/1.1.0.txt
rename to lifecycle/runtime/api_legacy/1.1.0.txt
diff --git a/lifecycle/runtime/api/1.0.0.txt b/lifecycle/runtime/api_legacy/current.txt
similarity index 100%
copy from lifecycle/runtime/api/1.0.0.txt
copy to lifecycle/runtime/api_legacy/current.txt
diff --git a/lifecycle/runtime/build.gradle b/lifecycle/runtime/build.gradle
index f7f6ff8..baa2b32 100644
--- a/lifecycle/runtime/build.gradle
+++ b/lifecycle/runtime/build.gradle
@@ -14,8 +14,8 @@
}
dependencies {
- api(project(":lifecycle:common"))
- api(project(":arch:common"))
+ api(project(":lifecycle:lifecycle-common"))
+ api(project(":arch:core-common"))
// necessary for IJ to resolve dependencies.
api(SUPPORT_ANNOTATIONS)
diff --git a/lifecycle/runtime/proguard-rules.pro b/lifecycle/runtime/proguard-rules.pro
index 5e61405..982e05b 100644
--- a/lifecycle/runtime/proguard-rules.pro
+++ b/lifecycle/runtime/proguard-rules.pro
@@ -1,16 +1,16 @@
-keepattributes *Annotation*
--keepclassmembers enum android.arch.lifecycle.Lifecycle$Event {
+-keepclassmembers enum androidx.lifecycle.Lifecycle$Event {
<fields>;
}
--keep class * implements android.arch.lifecycle.LifecycleObserver {
+-keep class * implements androidx.lifecycle.LifecycleObserver {
}
--keep class * implements android.arch.lifecycle.GeneratedAdapter {
+-keep class * implements androidx.lifecycle.GeneratedAdapter {
<init>(...);
}
-keepclassmembers class ** {
- @android.arch.lifecycle.OnLifecycleEvent *;
+ @androidx.lifecycle.OnLifecycleEvent *;
}
\ No newline at end of file
diff --git a/lifecycle/runtime/src/androidTest/java/android/arch/lifecycle/MissingClassTest.java b/lifecycle/runtime/src/androidTest/java/android/arch/lifecycle/MissingClassTest.java
deleted file mode 100644
index 81a0756..0000000
--- a/lifecycle/runtime/src/androidTest/java/android/arch/lifecycle/MissingClassTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.app.PictureInPictureParams;
-import android.os.Build;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
-@SmallTest
-public class MissingClassTest {
- public static class ObserverWithMissingClasses {
- @SuppressWarnings("unused")
- public void newApiMethod(PictureInPictureParams params) {}
-
- @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
- public void onResume() {}
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testMissingApi() {
- new ReflectiveGenericLifecycleObserver(new ObserverWithMissingClasses());
- }
-}
diff --git a/lifecycle/runtime/src/androidTest/java/androidx/lifecycle/MissingClassTest.java b/lifecycle/runtime/src/androidTest/java/androidx/lifecycle/MissingClassTest.java
new file mode 100644
index 0000000..1992f79
--- /dev/null
+++ b/lifecycle/runtime/src/androidTest/java/androidx/lifecycle/MissingClassTest.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 androidx.lifecycle;
+
+import android.app.PictureInPictureParams;
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
+@SmallTest
+public class MissingClassTest {
+ public static class ObserverWithMissingClasses {
+ @SuppressWarnings("unused")
+ public void newApiMethod(PictureInPictureParams params) {}
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ public void onResume() {}
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testMissingApi() {
+ new ReflectiveGenericLifecycleObserver(new ObserverWithMissingClasses());
+ }
+}
diff --git a/lifecycle/runtime/src/main/AndroidManifest.xml b/lifecycle/runtime/src/main/AndroidManifest.xml
index 274a076..d0df60e 100644
--- a/lifecycle/runtime/src/main/AndroidManifest.xml
+++ b/lifecycle/runtime/src/main/AndroidManifest.xml
@@ -16,5 +16,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle">
+ package="androidx.lifecycle">
</manifest>
diff --git a/lifecycle/runtime/src/main/java/android/arch/lifecycle/LifecycleRegistry.java b/lifecycle/runtime/src/main/java/android/arch/lifecycle/LifecycleRegistry.java
deleted file mode 100644
index eff946b..0000000
--- a/lifecycle/runtime/src/main/java/android/arch/lifecycle/LifecycleRegistry.java
+++ /dev/null
@@ -1,358 +0,0 @@
-/*
- * 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.DESTROYED;
-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 android.arch.core.internal.FastSafeIterableMap;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.util.Log;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Map.Entry;
-
-/**
- * An implementation of {@link Lifecycle} that can handle multiple observers.
- * <p>
- * It is used by Fragments and Support Library Activities. You can also directly use it if you have
- * a custom LifecycleOwner.
- */
-public class LifecycleRegistry extends Lifecycle {
-
- private static final String LOG_TAG = "LifecycleRegistry";
-
- /**
- * Custom list that keeps observers and can handle removals / additions during traversal.
- *
- * Invariant: at any moment of time for observer1 & observer2:
- * if addition_order(observer1) < addition_order(observer2), then
- * state(observer1) >= state(observer2),
- */
- private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
- new FastSafeIterableMap<>();
- /**
- * Current state
- */
- private State mState;
- /**
- * The provider that owns this Lifecycle.
- * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
- * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
- * because it keeps strong references on all other listeners, so you'll leak all of them as
- * well.
- */
- private final WeakReference<LifecycleOwner> mLifecycleOwner;
-
- private int mAddingObserverCounter = 0;
-
- private boolean mHandlingEvent = false;
- private boolean mNewEventOccurred = false;
-
- // we have to keep it for cases:
- // void onStart() {
- // mRegistry.removeObserver(this);
- // mRegistry.add(newObserver);
- // }
- // newObserver should be brought only to CREATED state during the execution of
- // this onStart method. our invariant with mObserverMap doesn't help, because parent observer
- // is no longer in the map.
- private ArrayList<State> mParentStates = new ArrayList<>();
-
- /**
- * Creates a new LifecycleRegistry for the given provider.
- * <p>
- * You should usually create this inside your LifecycleOwner class's constructor and hold
- * onto the same instance.
- *
- * @param provider The owner LifecycleOwner
- */
- public LifecycleRegistry(@NonNull LifecycleOwner provider) {
- mLifecycleOwner = new WeakReference<>(provider);
- mState = INITIALIZED;
- }
-
- /**
- * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
- *
- * @param state new state
- */
- @SuppressWarnings("WeakerAccess")
- @MainThread
- public void markState(@NonNull State state) {
- moveToState(state);
- }
-
- /**
- * Sets the current state and notifies the observers.
- * <p>
- * Note that if the {@code currentState} is the same state as the last call to this method,
- * calling this method has no effect.
- *
- * @param event The event that was received
- */
- public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
- State next = getStateAfter(event);
- moveToState(next);
- }
-
- private void moveToState(State next) {
- if (mState == next) {
- return;
- }
- mState = next;
- if (mHandlingEvent || mAddingObserverCounter != 0) {
- mNewEventOccurred = true;
- // we will figure out what to do on upper level.
- return;
- }
- mHandlingEvent = true;
- sync();
- mHandlingEvent = false;
- }
-
- private boolean isSynced() {
- if (mObserverMap.size() == 0) {
- return true;
- }
- State eldestObserverState = mObserverMap.eldest().getValue().mState;
- State newestObserverState = mObserverMap.newest().getValue().mState;
- return eldestObserverState == newestObserverState && mState == newestObserverState;
- }
-
- private State calculateTargetState(LifecycleObserver observer) {
- Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);
-
- State siblingState = previous != null ? previous.getValue().mState : null;
- State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
- : null;
- return min(min(mState, siblingState), parentState);
- }
-
- @Override
- public void addObserver(@NonNull LifecycleObserver observer) {
- State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
- ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
- ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
-
- if (previous != null) {
- return;
- }
- LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
- if (lifecycleOwner == null) {
- // it is null we should be destroyed. Fallback quickly
- return;
- }
-
- boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
- State targetState = calculateTargetState(observer);
- mAddingObserverCounter++;
- while ((statefulObserver.mState.compareTo(targetState) < 0
- && mObserverMap.contains(observer))) {
- pushParentState(statefulObserver.mState);
- statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
- popParentState();
- // mState / subling may have been changed recalculate
- targetState = calculateTargetState(observer);
- }
-
- if (!isReentrance) {
- // we do sync only on the top level.
- sync();
- }
- mAddingObserverCounter--;
- }
-
- private void popParentState() {
- mParentStates.remove(mParentStates.size() - 1);
- }
-
- private void pushParentState(State state) {
- mParentStates.add(state);
- }
-
- @Override
- public void removeObserver(@NonNull LifecycleObserver observer) {
- // we consciously decided not to send destruction events here in opposition to addObserver.
- // Our reasons for that:
- // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
- // actually occurred but earlier.
- // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
- // event. If removeObserver method sends destruction events, then a clean up routine becomes
- // more cumbersome. More specific example of that is: your LifecycleObserver listens for
- // a web connection, in the usual routine in OnStop method you report to a server that a
- // session has just ended and you close the connection. Now let's assume now that you
- // lost an internet and as a result you removed this observer. If you get destruction
- // events in removeObserver, you should have a special case in your onStop method that
- // checks if your web connection died and you shouldn't try to report anything to a server.
- mObserverMap.remove(observer);
- }
-
- /**
- * The number of observers.
- *
- * @return The number of observers.
- */
- @SuppressWarnings("WeakerAccess")
- public int getObserverCount() {
- return mObserverMap.size();
- }
-
- @NonNull
- @Override
- public State getCurrentState() {
- return mState;
- }
-
- static State getStateAfter(Event event) {
- switch (event) {
- case ON_CREATE:
- case ON_STOP:
- return CREATED;
- case ON_START:
- case ON_PAUSE:
- return STARTED;
- case ON_RESUME:
- return RESUMED;
- case ON_DESTROY:
- return DESTROYED;
- case ON_ANY:
- break;
- }
- throw new IllegalArgumentException("Unexpected event value " + event);
- }
-
- private static Event downEvent(State state) {
- switch (state) {
- case INITIALIZED:
- throw new IllegalArgumentException();
- case CREATED:
- return ON_DESTROY;
- case STARTED:
- return ON_STOP;
- case RESUMED:
- return ON_PAUSE;
- case DESTROYED:
- throw new IllegalArgumentException();
- }
- throw new IllegalArgumentException("Unexpected state value " + state);
- }
-
- private static Event upEvent(State state) {
- switch (state) {
- case INITIALIZED:
- case DESTROYED:
- return ON_CREATE;
- case CREATED:
- return ON_START;
- case STARTED:
- return ON_RESUME;
- case RESUMED:
- throw new IllegalArgumentException();
- }
- throw new IllegalArgumentException("Unexpected state value " + state);
- }
-
- private void forwardPass(LifecycleOwner lifecycleOwner) {
- Iterator<Entry<LifecycleObserver, ObserverWithState>> ascendingIterator =
- mObserverMap.iteratorWithAdditions();
- while (ascendingIterator.hasNext() && !mNewEventOccurred) {
- Entry<LifecycleObserver, ObserverWithState> entry = ascendingIterator.next();
- ObserverWithState observer = entry.getValue();
- while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred
- && mObserverMap.contains(entry.getKey()))) {
- pushParentState(observer.mState);
- observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState));
- popParentState();
- }
- }
- }
-
- private void backwardPass(LifecycleOwner lifecycleOwner) {
- Iterator<Entry<LifecycleObserver, ObserverWithState>> descendingIterator =
- mObserverMap.descendingIterator();
- while (descendingIterator.hasNext() && !mNewEventOccurred) {
- Entry<LifecycleObserver, ObserverWithState> entry = descendingIterator.next();
- ObserverWithState observer = entry.getValue();
- while ((observer.mState.compareTo(mState) > 0 && !mNewEventOccurred
- && mObserverMap.contains(entry.getKey()))) {
- Event event = downEvent(observer.mState);
- pushParentState(getStateAfter(event));
- observer.dispatchEvent(lifecycleOwner, event);
- popParentState();
- }
- }
- }
-
- // happens only on the top of stack (never in reentrance),
- // so it doesn't have to take in account parents
- private void sync() {
- LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
- if (lifecycleOwner == null) {
- Log.w(LOG_TAG, "LifecycleOwner is garbage collected, you shouldn't try dispatch "
- + "new events from it.");
- return;
- }
- while (!isSynced()) {
- mNewEventOccurred = false;
- // no need to check eldest for nullability, because isSynced does it for us.
- if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
- backwardPass(lifecycleOwner);
- }
- Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
- if (!mNewEventOccurred && newest != null
- && mState.compareTo(newest.getValue().mState) > 0) {
- forwardPass(lifecycleOwner);
- }
- }
- mNewEventOccurred = false;
- }
-
- static State min(@NonNull State state1, @Nullable State state2) {
- return state2 != null && state2.compareTo(state1) < 0 ? state2 : state1;
- }
-
- static class ObserverWithState {
- State mState;
- GenericLifecycleObserver mLifecycleObserver;
-
- ObserverWithState(LifecycleObserver observer, State initialState) {
- mLifecycleObserver = Lifecycling.getCallback(observer);
- mState = initialState;
- }
-
- void dispatchEvent(LifecycleOwner owner, Event event) {
- State newState = getStateAfter(event);
- mState = min(mState, newState);
- mLifecycleObserver.onStateChanged(owner, event);
- mState = newState;
- }
- }
-}
diff --git a/lifecycle/runtime/src/main/java/android/arch/lifecycle/LifecycleRegistryOwner.java b/lifecycle/runtime/src/main/java/android/arch/lifecycle/LifecycleRegistryOwner.java
deleted file mode 100644
index 0c67fef..0000000
--- a/lifecycle/runtime/src/main/java/android/arch/lifecycle/LifecycleRegistryOwner.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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;
-
-/**
- * @deprecated Use {@code android.support.v7.app.AppCompatActivity}
- * which extends {@link LifecycleOwner}, so there are no use cases for this class.
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-@Deprecated
-public interface LifecycleRegistryOwner extends LifecycleOwner {
- @NonNull
- @Override
- LifecycleRegistry getLifecycle();
-}
diff --git a/lifecycle/runtime/src/main/java/android/arch/lifecycle/ReportFragment.java b/lifecycle/runtime/src/main/java/android/arch/lifecycle/ReportFragment.java
deleted file mode 100644
index 16a89ce..0000000
--- a/lifecycle/runtime/src/main/java/android/arch/lifecycle/ReportFragment.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.app.Activity;
-import android.app.Fragment;
-import android.os.Bundle;
-import android.support.annotation.RestrictTo;
-
-/**
- * Internal class that dispatches initialization events.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ReportFragment extends Fragment {
- private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle"
- + ".LifecycleDispatcher.report_fragment_tag";
-
- public static void injectIfNeededIn(Activity activity) {
- // ProcessLifecycleOwner should always correctly work and some activities may not extend
- // FragmentActivity from support lib, so we use framework fragments for activities
- android.app.FragmentManager manager = activity.getFragmentManager();
- if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
- manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
- // Hopefully, we are the first to make a transaction.
- manager.executePendingTransactions();
- }
- }
-
- static ReportFragment get(Activity activity) {
- return (ReportFragment) activity.getFragmentManager().findFragmentByTag(
- REPORT_FRAGMENT_TAG);
- }
-
- private ActivityInitializationListener mProcessListener;
-
- private void dispatchCreate(ActivityInitializationListener listener) {
- if (listener != null) {
- listener.onCreate();
- }
- }
-
- private void dispatchStart(ActivityInitializationListener listener) {
- if (listener != null) {
- listener.onStart();
- }
- }
-
- private void dispatchResume(ActivityInitializationListener listener) {
- if (listener != null) {
- listener.onResume();
- }
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- dispatchCreate(mProcessListener);
- dispatch(Lifecycle.Event.ON_CREATE);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- dispatchStart(mProcessListener);
- dispatch(Lifecycle.Event.ON_START);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dispatchResume(mProcessListener);
- dispatch(Lifecycle.Event.ON_RESUME);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- dispatch(Lifecycle.Event.ON_PAUSE);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- dispatch(Lifecycle.Event.ON_STOP);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- dispatch(Lifecycle.Event.ON_DESTROY);
- // just want to be sure that we won't leak reference to an activity
- mProcessListener = null;
- }
-
- private void dispatch(Lifecycle.Event event) {
- Activity activity = getActivity();
- if (activity instanceof LifecycleRegistryOwner) {
- ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
- return;
- }
-
- if (activity instanceof LifecycleOwner) {
- Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
- if (lifecycle instanceof LifecycleRegistry) {
- ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
- }
- }
- }
-
- void setProcessListener(ActivityInitializationListener processListener) {
- mProcessListener = processListener;
- }
-
- interface ActivityInitializationListener {
- void onCreate();
-
- void onStart();
-
- void onResume();
- }
-}
diff --git a/lifecycle/runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java b/lifecycle/runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
new file mode 100644
index 0000000..3dcc2bb
--- /dev/null
+++ b/lifecycle/runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
@@ -0,0 +1,359 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
+import static androidx.lifecycle.Lifecycle.State.DESTROYED;
+import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
+import static androidx.lifecycle.Lifecycle.State.RESUMED;
+import static androidx.lifecycle.Lifecycle.State.STARTED;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.internal.FastSafeIterableMap;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+/**
+ * An implementation of {@link Lifecycle} that can handle multiple observers.
+ * <p>
+ * It is used by Fragments and Support Library Activities. You can also directly use it if you have
+ * a custom LifecycleOwner.
+ */
+public class LifecycleRegistry extends Lifecycle {
+
+ private static final String LOG_TAG = "LifecycleRegistry";
+
+ /**
+ * Custom list that keeps observers and can handle removals / additions during traversal.
+ *
+ * Invariant: at any moment of time for observer1 & observer2:
+ * if addition_order(observer1) < addition_order(observer2), then
+ * state(observer1) >= state(observer2),
+ */
+ private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
+ new FastSafeIterableMap<>();
+ /**
+ * Current state
+ */
+ private State mState;
+ /**
+ * The provider that owns this Lifecycle.
+ * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
+ * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
+ * because it keeps strong references on all other listeners, so you'll leak all of them as
+ * well.
+ */
+ private final WeakReference<LifecycleOwner> mLifecycleOwner;
+
+ private int mAddingObserverCounter = 0;
+
+ private boolean mHandlingEvent = false;
+ private boolean mNewEventOccurred = false;
+
+ // we have to keep it for cases:
+ // void onStart() {
+ // mRegistry.removeObserver(this);
+ // mRegistry.add(newObserver);
+ // }
+ // newObserver should be brought only to CREATED state during the execution of
+ // this onStart method. our invariant with mObserverMap doesn't help, because parent observer
+ // is no longer in the map.
+ private ArrayList<State> mParentStates = new ArrayList<>();
+
+ /**
+ * Creates a new LifecycleRegistry for the given provider.
+ * <p>
+ * You should usually create this inside your LifecycleOwner class's constructor and hold
+ * onto the same instance.
+ *
+ * @param provider The owner LifecycleOwner
+ */
+ public LifecycleRegistry(@NonNull LifecycleOwner provider) {
+ mLifecycleOwner = new WeakReference<>(provider);
+ mState = INITIALIZED;
+ }
+
+ /**
+ * Moves the Lifecycle to the given state and dispatches necessary events to the observers.
+ *
+ * @param state new state
+ */
+ @SuppressWarnings("WeakerAccess")
+ @MainThread
+ public void markState(@NonNull State state) {
+ moveToState(state);
+ }
+
+ /**
+ * Sets the current state and notifies the observers.
+ * <p>
+ * Note that if the {@code currentState} is the same state as the last call to this method,
+ * calling this method has no effect.
+ *
+ * @param event The event that was received
+ */
+ public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
+ State next = getStateAfter(event);
+ moveToState(next);
+ }
+
+ private void moveToState(State next) {
+ if (mState == next) {
+ return;
+ }
+ mState = next;
+ if (mHandlingEvent || mAddingObserverCounter != 0) {
+ mNewEventOccurred = true;
+ // we will figure out what to do on upper level.
+ return;
+ }
+ mHandlingEvent = true;
+ sync();
+ mHandlingEvent = false;
+ }
+
+ private boolean isSynced() {
+ if (mObserverMap.size() == 0) {
+ return true;
+ }
+ State eldestObserverState = mObserverMap.eldest().getValue().mState;
+ State newestObserverState = mObserverMap.newest().getValue().mState;
+ return eldestObserverState == newestObserverState && mState == newestObserverState;
+ }
+
+ private State calculateTargetState(LifecycleObserver observer) {
+ Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);
+
+ State siblingState = previous != null ? previous.getValue().mState : null;
+ State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
+ : null;
+ return min(min(mState, siblingState), parentState);
+ }
+
+ @Override
+ public void addObserver(@NonNull LifecycleObserver observer) {
+ State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
+ ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
+ ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
+
+ if (previous != null) {
+ return;
+ }
+ LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
+ if (lifecycleOwner == null) {
+ // it is null we should be destroyed. Fallback quickly
+ return;
+ }
+
+ boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
+ State targetState = calculateTargetState(observer);
+ mAddingObserverCounter++;
+ while ((statefulObserver.mState.compareTo(targetState) < 0
+ && mObserverMap.contains(observer))) {
+ pushParentState(statefulObserver.mState);
+ statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
+ popParentState();
+ // mState / subling may have been changed recalculate
+ targetState = calculateTargetState(observer);
+ }
+
+ if (!isReentrance) {
+ // we do sync only on the top level.
+ sync();
+ }
+ mAddingObserverCounter--;
+ }
+
+ private void popParentState() {
+ mParentStates.remove(mParentStates.size() - 1);
+ }
+
+ private void pushParentState(State state) {
+ mParentStates.add(state);
+ }
+
+ @Override
+ public void removeObserver(@NonNull LifecycleObserver observer) {
+ // we consciously decided not to send destruction events here in opposition to addObserver.
+ // Our reasons for that:
+ // 1. These events haven't yet happened at all. In contrast to events in addObservers, that
+ // actually occurred but earlier.
+ // 2. There are cases when removeObserver happens as a consequence of some kind of fatal
+ // event. If removeObserver method sends destruction events, then a clean up routine becomes
+ // more cumbersome. More specific example of that is: your LifecycleObserver listens for
+ // a web connection, in the usual routine in OnStop method you report to a server that a
+ // session has just ended and you close the connection. Now let's assume now that you
+ // lost an internet and as a result you removed this observer. If you get destruction
+ // events in removeObserver, you should have a special case in your onStop method that
+ // checks if your web connection died and you shouldn't try to report anything to a server.
+ mObserverMap.remove(observer);
+ }
+
+ /**
+ * The number of observers.
+ *
+ * @return The number of observers.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public int getObserverCount() {
+ return mObserverMap.size();
+ }
+
+ @NonNull
+ @Override
+ public State getCurrentState() {
+ return mState;
+ }
+
+ static State getStateAfter(Event event) {
+ switch (event) {
+ case ON_CREATE:
+ case ON_STOP:
+ return CREATED;
+ case ON_START:
+ case ON_PAUSE:
+ return STARTED;
+ case ON_RESUME:
+ return RESUMED;
+ case ON_DESTROY:
+ return DESTROYED;
+ case ON_ANY:
+ break;
+ }
+ throw new IllegalArgumentException("Unexpected event value " + event);
+ }
+
+ private static Event downEvent(State state) {
+ switch (state) {
+ case INITIALIZED:
+ throw new IllegalArgumentException();
+ case CREATED:
+ return ON_DESTROY;
+ case STARTED:
+ return ON_STOP;
+ case RESUMED:
+ return ON_PAUSE;
+ case DESTROYED:
+ throw new IllegalArgumentException();
+ }
+ throw new IllegalArgumentException("Unexpected state value " + state);
+ }
+
+ private static Event upEvent(State state) {
+ switch (state) {
+ case INITIALIZED:
+ case DESTROYED:
+ return ON_CREATE;
+ case CREATED:
+ return ON_START;
+ case STARTED:
+ return ON_RESUME;
+ case RESUMED:
+ throw new IllegalArgumentException();
+ }
+ throw new IllegalArgumentException("Unexpected state value " + state);
+ }
+
+ private void forwardPass(LifecycleOwner lifecycleOwner) {
+ Iterator<Entry<LifecycleObserver, ObserverWithState>> ascendingIterator =
+ mObserverMap.iteratorWithAdditions();
+ while (ascendingIterator.hasNext() && !mNewEventOccurred) {
+ Entry<LifecycleObserver, ObserverWithState> entry = ascendingIterator.next();
+ ObserverWithState observer = entry.getValue();
+ while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred
+ && mObserverMap.contains(entry.getKey()))) {
+ pushParentState(observer.mState);
+ observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState));
+ popParentState();
+ }
+ }
+ }
+
+ private void backwardPass(LifecycleOwner lifecycleOwner) {
+ Iterator<Entry<LifecycleObserver, ObserverWithState>> descendingIterator =
+ mObserverMap.descendingIterator();
+ while (descendingIterator.hasNext() && !mNewEventOccurred) {
+ Entry<LifecycleObserver, ObserverWithState> entry = descendingIterator.next();
+ ObserverWithState observer = entry.getValue();
+ while ((observer.mState.compareTo(mState) > 0 && !mNewEventOccurred
+ && mObserverMap.contains(entry.getKey()))) {
+ Event event = downEvent(observer.mState);
+ pushParentState(getStateAfter(event));
+ observer.dispatchEvent(lifecycleOwner, event);
+ popParentState();
+ }
+ }
+ }
+
+ // happens only on the top of stack (never in reentrance),
+ // so it doesn't have to take in account parents
+ private void sync() {
+ LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
+ if (lifecycleOwner == null) {
+ Log.w(LOG_TAG, "LifecycleOwner is garbage collected, you shouldn't try dispatch "
+ + "new events from it.");
+ return;
+ }
+ while (!isSynced()) {
+ mNewEventOccurred = false;
+ // no need to check eldest for nullability, because isSynced does it for us.
+ if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
+ backwardPass(lifecycleOwner);
+ }
+ Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
+ if (!mNewEventOccurred && newest != null
+ && mState.compareTo(newest.getValue().mState) > 0) {
+ forwardPass(lifecycleOwner);
+ }
+ }
+ mNewEventOccurred = false;
+ }
+
+ static State min(@NonNull State state1, @Nullable State state2) {
+ return state2 != null && state2.compareTo(state1) < 0 ? state2 : state1;
+ }
+
+ static class ObserverWithState {
+ State mState;
+ GenericLifecycleObserver mLifecycleObserver;
+
+ ObserverWithState(LifecycleObserver observer, State initialState) {
+ mLifecycleObserver = Lifecycling.getCallback(observer);
+ mState = initialState;
+ }
+
+ void dispatchEvent(LifecycleOwner owner, Event event) {
+ State newState = getStateAfter(event);
+ mState = min(mState, newState);
+ mLifecycleObserver.onStateChanged(owner, event);
+ mState = newState;
+ }
+ }
+}
diff --git a/lifecycle/runtime/src/main/java/androidx/lifecycle/LifecycleRegistryOwner.java b/lifecycle/runtime/src/main/java/androidx/lifecycle/LifecycleRegistryOwner.java
new file mode 100644
index 0000000..630b98b
--- /dev/null
+++ b/lifecycle/runtime/src/main/java/androidx/lifecycle/LifecycleRegistryOwner.java
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.NonNull;
+
+/**
+ * @deprecated Use {@code android.support.v7.app.AppCompatActivity}
+ * which extends {@link LifecycleOwner}, so there are no use cases for this class.
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@Deprecated
+public interface LifecycleRegistryOwner extends LifecycleOwner {
+ @NonNull
+ @Override
+ LifecycleRegistry getLifecycle();
+}
diff --git a/lifecycle/runtime/src/main/java/androidx/lifecycle/ReportFragment.java b/lifecycle/runtime/src/main/java/androidx/lifecycle/ReportFragment.java
new file mode 100644
index 0000000..33c7771
--- /dev/null
+++ b/lifecycle/runtime/src/main/java/androidx/lifecycle/ReportFragment.java
@@ -0,0 +1,138 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.os.Bundle;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Internal class that dispatches initialization events.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ReportFragment extends Fragment {
+ private static final String REPORT_FRAGMENT_TAG = "androidx.lifecycle"
+ + ".LifecycleDispatcher.report_fragment_tag";
+
+ public static void injectIfNeededIn(Activity activity) {
+ // ProcessLifecycleOwner should always correctly work and some activities may not extend
+ // FragmentActivity from support lib, so we use framework fragments for activities
+ android.app.FragmentManager manager = activity.getFragmentManager();
+ if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
+ manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
+ // Hopefully, we are the first to make a transaction.
+ manager.executePendingTransactions();
+ }
+ }
+
+ static ReportFragment get(Activity activity) {
+ return (ReportFragment) activity.getFragmentManager().findFragmentByTag(
+ REPORT_FRAGMENT_TAG);
+ }
+
+ private ActivityInitializationListener mProcessListener;
+
+ private void dispatchCreate(ActivityInitializationListener listener) {
+ if (listener != null) {
+ listener.onCreate();
+ }
+ }
+
+ private void dispatchStart(ActivityInitializationListener listener) {
+ if (listener != null) {
+ listener.onStart();
+ }
+ }
+
+ private void dispatchResume(ActivityInitializationListener listener) {
+ if (listener != null) {
+ listener.onResume();
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ dispatchCreate(mProcessListener);
+ dispatch(Lifecycle.Event.ON_CREATE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ dispatchStart(mProcessListener);
+ dispatch(Lifecycle.Event.ON_START);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ dispatchResume(mProcessListener);
+ dispatch(Lifecycle.Event.ON_RESUME);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ dispatch(Lifecycle.Event.ON_PAUSE);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ dispatch(Lifecycle.Event.ON_STOP);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ dispatch(Lifecycle.Event.ON_DESTROY);
+ // just want to be sure that we won't leak reference to an activity
+ mProcessListener = null;
+ }
+
+ private void dispatch(Lifecycle.Event event) {
+ Activity activity = getActivity();
+ if (activity instanceof LifecycleRegistryOwner) {
+ ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
+ return;
+ }
+
+ if (activity instanceof LifecycleOwner) {
+ Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
+ if (lifecycle instanceof LifecycleRegistry) {
+ ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
+ }
+ }
+ }
+
+ void setProcessListener(ActivityInitializationListener processListener) {
+ mProcessListener = processListener;
+ }
+
+ interface ActivityInitializationListener {
+ void onCreate();
+
+ void onStart();
+
+ void onResume();
+ }
+}
diff --git a/lifecycle/runtime/src/test/java/NoPackageObserver.java b/lifecycle/runtime/src/test/java/NoPackageObserver.java
index 58f9fba..7452d7c 100644
--- a/lifecycle/runtime/src/test/java/NoPackageObserver.java
+++ b/lifecycle/runtime/src/test/java/NoPackageObserver.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
-import android.arch.lifecycle.LifecycleObserver;
-import android.arch.lifecycle.OnLifecycleEvent;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
class NoPackageObserver implements LifecycleObserver {
@OnLifecycleEvent(ON_CREATE)
diff --git a/lifecycle/runtime/src/test/java/NoPackageTest.java b/lifecycle/runtime/src/test/java/NoPackageTest.java
index 8763202..e84e87d 100644
--- a/lifecycle/runtime/src/test/java/NoPackageTest.java
+++ b/lifecycle/runtime/src/test/java/NoPackageTest.java
@@ -14,15 +14,15 @@
* limitations under the License.
*/
-import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
import org.junit.Before;
import org.junit.Test;
diff --git a/lifecycle/runtime/src/test/java/android/arch/lifecycle/LifecycleRegistryTest.java b/lifecycle/runtime/src/test/java/android/arch/lifecycle/LifecycleRegistryTest.java
deleted file mode 100644
index 2a7bbad..0000000
--- a/lifecycle/runtime/src/test/java/android/arch/lifecycle/LifecycleRegistryTest.java
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * 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 org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-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;
-
-@RunWith(JUnit4.class)
-public class LifecycleRegistryTest {
- private LifecycleOwner mLifecycleOwner;
- private Lifecycle mLifecycle;
- private LifecycleRegistry mRegistry;
-
- @Before
- public void init() {
- mLifecycleOwner = mock(LifecycleOwner.class);
- mLifecycle = mock(Lifecycle.class);
- when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycle);
- mRegistry = new LifecycleRegistry(mLifecycleOwner);
- }
-
- @Test
- public void addRemove() {
- LifecycleObserver observer = mock(LifecycleObserver.class);
- mRegistry.addObserver(observer);
- assertThat(mRegistry.getObserverCount(), is(1));
- mRegistry.removeObserver(observer);
- assertThat(mRegistry.getObserverCount(), is(0));
- }
-
- @Test
- public void addGenericAndObserve() {
- GenericLifecycleObserver generic = mock(GenericLifecycleObserver.class);
- mRegistry.addObserver(generic);
- dispatchEvent(ON_CREATE);
- verify(generic).onStateChanged(mLifecycleOwner, ON_CREATE);
- reset(generic);
- dispatchEvent(ON_CREATE);
- verify(generic, never()).onStateChanged(mLifecycleOwner, ON_CREATE);
- }
-
- @Test
- public void addRegularClass() {
- TestObserver testObserver = mock(TestObserver.class);
- mRegistry.addObserver(testObserver);
- dispatchEvent(ON_START);
- verify(testObserver, never()).onStop();
- dispatchEvent(ON_STOP);
- verify(testObserver).onStop();
- }
-
- @Test
- public void add2RemoveOne() {
- TestObserver observer1 = mock(TestObserver.class);
- TestObserver observer2 = mock(TestObserver.class);
- TestObserver observer3 = mock(TestObserver.class);
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
- mRegistry.addObserver(observer3);
-
- dispatchEvent(ON_CREATE);
-
- verify(observer1).onCreate();
- verify(observer2).onCreate();
- verify(observer3).onCreate();
- reset(observer1, observer2, observer3);
-
- mRegistry.removeObserver(observer2);
- dispatchEvent(ON_START);
-
- verify(observer1).onStart();
- verify(observer2, never()).onStart();
- verify(observer3).onStart();
- }
-
- @Test
- public void removeWhileTraversing() {
- final TestObserver observer2 = mock(TestObserver.class);
- TestObserver observer1 = spy(new TestObserver() {
- @Override
- public void onCreate() {
- mRegistry.removeObserver(observer2);
- }
- });
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
- dispatchEvent(ON_CREATE);
- verify(observer2, never()).onCreate();
- verify(observer1).onCreate();
- }
-
- @Test
- public void constructionOrder() {
- fullyInitializeRegistry();
- final TestObserver observer = mock(TestObserver.class);
- mRegistry.addObserver(observer);
- InOrder inOrder = inOrder(observer);
- inOrder.verify(observer).onCreate();
- inOrder.verify(observer).onStart();
- inOrder.verify(observer).onResume();
- }
-
- @Test
- public void constructionDestruction1() {
- fullyInitializeRegistry();
- final TestObserver observer = spy(new TestObserver() {
- @Override
- void onStart() {
- dispatchEvent(ON_PAUSE);
- }
- });
- mRegistry.addObserver(observer);
- InOrder constructionOrder = inOrder(observer);
- constructionOrder.verify(observer).onCreate();
- constructionOrder.verify(observer).onStart();
- constructionOrder.verify(observer, never()).onResume();
- }
-
- @Test
- public void constructionDestruction2() {
- fullyInitializeRegistry();
- final TestObserver observer = spy(new TestObserver() {
- @Override
- void onStart() {
- dispatchEvent(ON_PAUSE);
- dispatchEvent(ON_STOP);
- dispatchEvent(ON_DESTROY);
- }
- });
- mRegistry.addObserver(observer);
- InOrder orderVerifier = inOrder(observer);
- orderVerifier.verify(observer).onCreate();
- orderVerifier.verify(observer).onStart();
- orderVerifier.verify(observer).onStop();
- orderVerifier.verify(observer).onDestroy();
- orderVerifier.verify(observer, never()).onResume();
- }
-
- @Test
- public void twoObserversChangingState() {
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onCreate() {
- dispatchEvent(ON_START);
- }
- });
- final TestObserver observer2 = mock(TestObserver.class);
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
- dispatchEvent(ON_CREATE);
- verify(observer1, times(1)).onCreate();
- verify(observer2, times(1)).onCreate();
- verify(observer1, times(1)).onStart();
- verify(observer2, times(1)).onStart();
- }
-
- @Test
- public void addDuringTraversing() {
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- public void onStart() {
- mRegistry.addObserver(observer3);
- }
- });
- final TestObserver observer2 = mock(TestObserver.class);
-
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
-
- dispatchEvent(ON_CREATE);
- dispatchEvent(ON_START);
-
- InOrder inOrder = inOrder(observer1, observer2, observer3);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer3).onStart();
- }
-
- @Test
- public void addDuringAddition() {
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer2 = spy(new TestObserver() {
- @Override
- public void onCreate() {
- mRegistry.addObserver(observer3);
- }
- });
-
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- public void onResume() {
- mRegistry.addObserver(observer2);
- }
- });
-
- mRegistry.addObserver(observer1);
-
- dispatchEvent(ON_CREATE);
- dispatchEvent(ON_START);
- dispatchEvent(ON_RESUME);
-
- InOrder inOrder = inOrder(observer1, observer2, observer3);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer1).onResume();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer2).onResume();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer3).onStart();
- inOrder.verify(observer3).onResume();
- }
-
- @Test
- public void subscribeToDead() {
- dispatchEvent(ON_CREATE);
- final TestObserver observer1 = mock(TestObserver.class);
- mRegistry.addObserver(observer1);
- verify(observer1).onCreate();
- dispatchEvent(ON_DESTROY);
- verify(observer1).onDestroy();
- final TestObserver observer2 = mock(TestObserver.class);
- mRegistry.addObserver(observer2);
- verify(observer2, never()).onCreate();
- reset(observer1);
- dispatchEvent(ON_CREATE);
- verify(observer1).onCreate();
- verify(observer2).onCreate();
- }
-
- @Test
- public void downEvents() {
- fullyInitializeRegistry();
- final TestObserver observer1 = mock(TestObserver.class);
- final TestObserver observer2 = mock(TestObserver.class);
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
- InOrder orderVerifier = inOrder(observer1, observer2);
- dispatchEvent(ON_PAUSE);
- orderVerifier.verify(observer2).onPause();
- orderVerifier.verify(observer1).onPause();
- dispatchEvent(ON_STOP);
- orderVerifier.verify(observer2).onStop();
- orderVerifier.verify(observer1).onStop();
- dispatchEvent(ON_DESTROY);
- orderVerifier.verify(observer2).onDestroy();
- orderVerifier.verify(observer1).onDestroy();
- }
-
- @Test
- public void downEventsAddition() {
- dispatchEvent(ON_CREATE);
- dispatchEvent(ON_START);
- final TestObserver observer1 = mock(TestObserver.class);
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer2 = spy(new TestObserver() {
- @Override
- void onStop() {
- mRegistry.addObserver(observer3);
- }
- });
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
- InOrder orderVerifier = inOrder(observer1, observer2, observer3);
- dispatchEvent(ON_STOP);
- orderVerifier.verify(observer2).onStop();
- orderVerifier.verify(observer3).onCreate();
- orderVerifier.verify(observer1).onStop();
- dispatchEvent(ON_DESTROY);
- orderVerifier.verify(observer3).onDestroy();
- orderVerifier.verify(observer2).onDestroy();
- orderVerifier.verify(observer1).onDestroy();
- }
-
- @Test
- public void downEventsRemoveAll() {
- fullyInitializeRegistry();
- final TestObserver observer1 = mock(TestObserver.class);
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer2 = spy(new TestObserver() {
- @Override
- void onStop() {
- mRegistry.removeObserver(observer3);
- mRegistry.removeObserver(this);
- mRegistry.removeObserver(observer1);
- assertThat(mRegistry.getObserverCount(), is(0));
- }
- });
- mRegistry.addObserver(observer1);
- mRegistry.addObserver(observer2);
- mRegistry.addObserver(observer3);
- InOrder orderVerifier = inOrder(observer1, observer2, observer3);
- dispatchEvent(ON_PAUSE);
- orderVerifier.verify(observer3).onPause();
- orderVerifier.verify(observer2).onPause();
- orderVerifier.verify(observer1).onPause();
- dispatchEvent(ON_STOP);
- orderVerifier.verify(observer3).onStop();
- orderVerifier.verify(observer2).onStop();
- orderVerifier.verify(observer1, never()).onStop();
- dispatchEvent(ON_PAUSE);
- orderVerifier.verify(observer3, never()).onPause();
- orderVerifier.verify(observer2, never()).onPause();
- orderVerifier.verify(observer1, never()).onPause();
- }
-
- @Test
- public void deadParentInAddition() {
- fullyInitializeRegistry();
- final TestObserver observer2 = mock(TestObserver.class);
- final TestObserver observer3 = mock(TestObserver.class);
-
- TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onStart() {
- mRegistry.removeObserver(this);
- assertThat(mRegistry.getObserverCount(), is(0));
- mRegistry.addObserver(observer2);
- mRegistry.addObserver(observer3);
- }
- });
-
- mRegistry.addObserver(observer1);
-
- InOrder inOrder = inOrder(observer1, observer2, observer3);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer2).onResume();
- inOrder.verify(observer3).onStart();
- inOrder.verify(observer3).onResume();
- }
-
- @Test
- public void deadParentWhileTraversing() {
- final TestObserver observer2 = mock(TestObserver.class);
- final TestObserver observer3 = mock(TestObserver.class);
- TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onStart() {
- mRegistry.removeObserver(this);
- assertThat(mRegistry.getObserverCount(), is(0));
- mRegistry.addObserver(observer2);
- mRegistry.addObserver(observer3);
- }
- });
- InOrder inOrder = inOrder(observer1, observer2, observer3);
- mRegistry.addObserver(observer1);
- dispatchEvent(ON_CREATE);
- dispatchEvent(ON_START);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer3).onStart();
- }
-
- @Test
- public void removeCascade() {
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer4 = mock(TestObserver.class);
-
- final TestObserver observer2 = spy(new TestObserver() {
- @Override
- void onStart() {
- mRegistry.removeObserver(this);
- }
- });
-
- TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onResume() {
- mRegistry.removeObserver(this);
- mRegistry.addObserver(observer2);
- mRegistry.addObserver(observer3);
- mRegistry.addObserver(observer4);
- }
- });
- fullyInitializeRegistry();
- mRegistry.addObserver(observer1);
- InOrder inOrder = inOrder(observer1, observer2, observer3, observer4);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer1).onResume();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer3).onStart();
- inOrder.verify(observer4).onCreate();
- inOrder.verify(observer4).onStart();
- inOrder.verify(observer3).onResume();
- inOrder.verify(observer4).onResume();
- }
-
- @Test
- public void changeStateDuringDescending() {
- final TestObserver observer2 = mock(TestObserver.class);
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onPause() {
- // but tonight I bounce back
- mRegistry.handleLifecycleEvent(ON_RESUME);
- mRegistry.addObserver(observer2);
- }
- });
- fullyInitializeRegistry();
- mRegistry.addObserver(observer1);
- mRegistry.handleLifecycleEvent(ON_PAUSE);
- InOrder inOrder = inOrder(observer1, observer2);
- inOrder.verify(observer1).onPause();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer1).onResume();
- inOrder.verify(observer2).onResume();
- }
-
- @Test
- public void siblingLimitationCheck() {
- fullyInitializeRegistry();
- final TestObserver observer2 = mock(TestObserver.class);
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onStart() {
- mRegistry.addObserver(observer2);
- }
-
- @Override
- void onResume() {
- mRegistry.addObserver(observer3);
- }
- });
- mRegistry.addObserver(observer1);
- InOrder inOrder = inOrder(observer1, observer2, observer3);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer1).onResume();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer2).onStart();
- inOrder.verify(observer2).onResume();
- inOrder.verify(observer3).onStart();
- inOrder.verify(observer3).onResume();
- }
-
- @Test
- public void siblingRemovalLimitationCheck1() {
- fullyInitializeRegistry();
- final TestObserver observer2 = mock(TestObserver.class);
- final TestObserver observer3 = mock(TestObserver.class);
- final TestObserver observer4 = mock(TestObserver.class);
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onStart() {
- mRegistry.addObserver(observer2);
- }
-
- @Override
- void onResume() {
- mRegistry.removeObserver(observer2);
- mRegistry.addObserver(observer3);
- mRegistry.addObserver(observer4);
- }
- });
- mRegistry.addObserver(observer1);
- InOrder inOrder = inOrder(observer1, observer2, observer3, observer4);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer1).onResume();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer3).onStart();
- inOrder.verify(observer4).onCreate();
- inOrder.verify(observer4).onStart();
- inOrder.verify(observer3).onResume();
- inOrder.verify(observer4).onResume();
- }
-
- @Test
- public void siblingRemovalLimitationCheck2() {
- fullyInitializeRegistry();
- final TestObserver observer2 = mock(TestObserver.class);
- final TestObserver observer3 = spy(new TestObserver() {
- @Override
- void onCreate() {
- mRegistry.removeObserver(observer2);
- }
- });
- final TestObserver observer4 = mock(TestObserver.class);
- final TestObserver observer1 = spy(new TestObserver() {
- @Override
- void onStart() {
- mRegistry.addObserver(observer2);
- }
-
- @Override
- void onResume() {
- mRegistry.addObserver(observer3);
- mRegistry.addObserver(observer4);
- }
- });
-
- mRegistry.addObserver(observer1);
- InOrder inOrder = inOrder(observer1, observer2, observer3, observer4);
- inOrder.verify(observer1).onCreate();
- inOrder.verify(observer1).onStart();
- inOrder.verify(observer2).onCreate();
- inOrder.verify(observer1).onResume();
- inOrder.verify(observer3).onCreate();
- inOrder.verify(observer3).onStart();
- inOrder.verify(observer4).onCreate();
- inOrder.verify(observer4).onStart();
- inOrder.verify(observer3).onResume();
- inOrder.verify(observer4).onResume();
- }
-
- @Test
- public void sameObserverReAddition() {
- TestObserver observer = mock(TestObserver.class);
- mRegistry.addObserver(observer);
- mRegistry.removeObserver(observer);
- mRegistry.addObserver(observer);
- dispatchEvent(ON_CREATE);
- verify(observer).onCreate();
- }
-
- private static void forceGc() {
- Runtime.getRuntime().gc();
- Runtime.getRuntime().runFinalization();
- Runtime.getRuntime().gc();
- Runtime.getRuntime().runFinalization();
- }
-
- @Test
- public void goneLifecycleOwner() {
- fullyInitializeRegistry();
- mLifecycleOwner = null;
- forceGc();
- TestObserver observer = mock(TestObserver.class);
- mRegistry.addObserver(observer);
- verify(observer, never()).onCreate();
- verify(observer, never()).onStart();
- verify(observer, never()).onResume();
- }
-
- private void dispatchEvent(Lifecycle.Event event) {
- when(mLifecycle.getCurrentState()).thenReturn(LifecycleRegistry.getStateAfter(event));
- mRegistry.handleLifecycleEvent(event);
- }
-
- private void fullyInitializeRegistry() {
- dispatchEvent(ON_CREATE);
- dispatchEvent(ON_START);
- dispatchEvent(ON_RESUME);
- }
-
- private abstract class TestObserver implements LifecycleObserver {
- @OnLifecycleEvent(ON_CREATE)
- void onCreate() {
- }
-
- @OnLifecycleEvent(ON_START)
- void onStart() {
- }
-
- @OnLifecycleEvent(ON_RESUME)
- void onResume() {
- }
-
- @OnLifecycleEvent(ON_PAUSE)
- void onPause() {
- }
-
- @OnLifecycleEvent(ON_STOP)
- void onStop() {
- }
-
- @OnLifecycleEvent(ON_DESTROY)
- void onDestroy() {
- }
- }
-}
diff --git a/lifecycle/runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java b/lifecycle/runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
new file mode 100644
index 0000000..ed1f7fa
--- /dev/null
+++ b/lifecycle/runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
@@ -0,0 +1,624 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+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;
+
+@RunWith(JUnit4.class)
+public class LifecycleRegistryTest {
+ private LifecycleOwner mLifecycleOwner;
+ private Lifecycle mLifecycle;
+ private LifecycleRegistry mRegistry;
+
+ @Before
+ public void init() {
+ mLifecycleOwner = mock(LifecycleOwner.class);
+ mLifecycle = mock(Lifecycle.class);
+ when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycle);
+ mRegistry = new LifecycleRegistry(mLifecycleOwner);
+ }
+
+ @Test
+ public void addRemove() {
+ LifecycleObserver observer = mock(LifecycleObserver.class);
+ mRegistry.addObserver(observer);
+ assertThat(mRegistry.getObserverCount(), is(1));
+ mRegistry.removeObserver(observer);
+ assertThat(mRegistry.getObserverCount(), is(0));
+ }
+
+ @Test
+ public void addGenericAndObserve() {
+ GenericLifecycleObserver generic = mock(GenericLifecycleObserver.class);
+ mRegistry.addObserver(generic);
+ dispatchEvent(ON_CREATE);
+ verify(generic).onStateChanged(mLifecycleOwner, ON_CREATE);
+ reset(generic);
+ dispatchEvent(ON_CREATE);
+ verify(generic, never()).onStateChanged(mLifecycleOwner, ON_CREATE);
+ }
+
+ @Test
+ public void addRegularClass() {
+ TestObserver testObserver = mock(TestObserver.class);
+ mRegistry.addObserver(testObserver);
+ dispatchEvent(ON_START);
+ verify(testObserver, never()).onStop();
+ dispatchEvent(ON_STOP);
+ verify(testObserver).onStop();
+ }
+
+ @Test
+ public void add2RemoveOne() {
+ TestObserver observer1 = mock(TestObserver.class);
+ TestObserver observer2 = mock(TestObserver.class);
+ TestObserver observer3 = mock(TestObserver.class);
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ mRegistry.addObserver(observer3);
+
+ dispatchEvent(ON_CREATE);
+
+ verify(observer1).onCreate();
+ verify(observer2).onCreate();
+ verify(observer3).onCreate();
+ reset(observer1, observer2, observer3);
+
+ mRegistry.removeObserver(observer2);
+ dispatchEvent(ON_START);
+
+ verify(observer1).onStart();
+ verify(observer2, never()).onStart();
+ verify(observer3).onStart();
+ }
+
+ @Test
+ public void removeWhileTraversing() {
+ final TestObserver observer2 = mock(TestObserver.class);
+ TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ public void onCreate() {
+ mRegistry.removeObserver(observer2);
+ }
+ });
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ dispatchEvent(ON_CREATE);
+ verify(observer2, never()).onCreate();
+ verify(observer1).onCreate();
+ }
+
+ @Test
+ public void constructionOrder() {
+ fullyInitializeRegistry();
+ final TestObserver observer = mock(TestObserver.class);
+ mRegistry.addObserver(observer);
+ InOrder inOrder = inOrder(observer);
+ inOrder.verify(observer).onCreate();
+ inOrder.verify(observer).onStart();
+ inOrder.verify(observer).onResume();
+ }
+
+ @Test
+ public void constructionDestruction1() {
+ fullyInitializeRegistry();
+ final TestObserver observer = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ dispatchEvent(ON_PAUSE);
+ }
+ });
+ mRegistry.addObserver(observer);
+ InOrder constructionOrder = inOrder(observer);
+ constructionOrder.verify(observer).onCreate();
+ constructionOrder.verify(observer).onStart();
+ constructionOrder.verify(observer, never()).onResume();
+ }
+
+ @Test
+ public void constructionDestruction2() {
+ fullyInitializeRegistry();
+ final TestObserver observer = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ dispatchEvent(ON_PAUSE);
+ dispatchEvent(ON_STOP);
+ dispatchEvent(ON_DESTROY);
+ }
+ });
+ mRegistry.addObserver(observer);
+ InOrder orderVerifier = inOrder(observer);
+ orderVerifier.verify(observer).onCreate();
+ orderVerifier.verify(observer).onStart();
+ orderVerifier.verify(observer).onStop();
+ orderVerifier.verify(observer).onDestroy();
+ orderVerifier.verify(observer, never()).onResume();
+ }
+
+ @Test
+ public void twoObserversChangingState() {
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onCreate() {
+ dispatchEvent(ON_START);
+ }
+ });
+ final TestObserver observer2 = mock(TestObserver.class);
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ dispatchEvent(ON_CREATE);
+ verify(observer1, times(1)).onCreate();
+ verify(observer2, times(1)).onCreate();
+ verify(observer1, times(1)).onStart();
+ verify(observer2, times(1)).onStart();
+ }
+
+ @Test
+ public void addDuringTraversing() {
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ public void onStart() {
+ mRegistry.addObserver(observer3);
+ }
+ });
+ final TestObserver observer2 = mock(TestObserver.class);
+
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+
+ dispatchEvent(ON_CREATE);
+ dispatchEvent(ON_START);
+
+ InOrder inOrder = inOrder(observer1, observer2, observer3);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer3).onStart();
+ }
+
+ @Test
+ public void addDuringAddition() {
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer2 = spy(new TestObserver() {
+ @Override
+ public void onCreate() {
+ mRegistry.addObserver(observer3);
+ }
+ });
+
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ public void onResume() {
+ mRegistry.addObserver(observer2);
+ }
+ });
+
+ mRegistry.addObserver(observer1);
+
+ dispatchEvent(ON_CREATE);
+ dispatchEvent(ON_START);
+ dispatchEvent(ON_RESUME);
+
+ InOrder inOrder = inOrder(observer1, observer2, observer3);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer1).onResume();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer2).onResume();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer3).onStart();
+ inOrder.verify(observer3).onResume();
+ }
+
+ @Test
+ public void subscribeToDead() {
+ dispatchEvent(ON_CREATE);
+ final TestObserver observer1 = mock(TestObserver.class);
+ mRegistry.addObserver(observer1);
+ verify(observer1).onCreate();
+ dispatchEvent(ON_DESTROY);
+ verify(observer1).onDestroy();
+ final TestObserver observer2 = mock(TestObserver.class);
+ mRegistry.addObserver(observer2);
+ verify(observer2, never()).onCreate();
+ reset(observer1);
+ dispatchEvent(ON_CREATE);
+ verify(observer1).onCreate();
+ verify(observer2).onCreate();
+ }
+
+ @Test
+ public void downEvents() {
+ fullyInitializeRegistry();
+ final TestObserver observer1 = mock(TestObserver.class);
+ final TestObserver observer2 = mock(TestObserver.class);
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ InOrder orderVerifier = inOrder(observer1, observer2);
+ dispatchEvent(ON_PAUSE);
+ orderVerifier.verify(observer2).onPause();
+ orderVerifier.verify(observer1).onPause();
+ dispatchEvent(ON_STOP);
+ orderVerifier.verify(observer2).onStop();
+ orderVerifier.verify(observer1).onStop();
+ dispatchEvent(ON_DESTROY);
+ orderVerifier.verify(observer2).onDestroy();
+ orderVerifier.verify(observer1).onDestroy();
+ }
+
+ @Test
+ public void downEventsAddition() {
+ dispatchEvent(ON_CREATE);
+ dispatchEvent(ON_START);
+ final TestObserver observer1 = mock(TestObserver.class);
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer2 = spy(new TestObserver() {
+ @Override
+ void onStop() {
+ mRegistry.addObserver(observer3);
+ }
+ });
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ InOrder orderVerifier = inOrder(observer1, observer2, observer3);
+ dispatchEvent(ON_STOP);
+ orderVerifier.verify(observer2).onStop();
+ orderVerifier.verify(observer3).onCreate();
+ orderVerifier.verify(observer1).onStop();
+ dispatchEvent(ON_DESTROY);
+ orderVerifier.verify(observer3).onDestroy();
+ orderVerifier.verify(observer2).onDestroy();
+ orderVerifier.verify(observer1).onDestroy();
+ }
+
+ @Test
+ public void downEventsRemoveAll() {
+ fullyInitializeRegistry();
+ final TestObserver observer1 = mock(TestObserver.class);
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer2 = spy(new TestObserver() {
+ @Override
+ void onStop() {
+ mRegistry.removeObserver(observer3);
+ mRegistry.removeObserver(this);
+ mRegistry.removeObserver(observer1);
+ assertThat(mRegistry.getObserverCount(), is(0));
+ }
+ });
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ mRegistry.addObserver(observer3);
+ InOrder orderVerifier = inOrder(observer1, observer2, observer3);
+ dispatchEvent(ON_PAUSE);
+ orderVerifier.verify(observer3).onPause();
+ orderVerifier.verify(observer2).onPause();
+ orderVerifier.verify(observer1).onPause();
+ dispatchEvent(ON_STOP);
+ orderVerifier.verify(observer3).onStop();
+ orderVerifier.verify(observer2).onStop();
+ orderVerifier.verify(observer1, never()).onStop();
+ dispatchEvent(ON_PAUSE);
+ orderVerifier.verify(observer3, never()).onPause();
+ orderVerifier.verify(observer2, never()).onPause();
+ orderVerifier.verify(observer1, never()).onPause();
+ }
+
+ @Test
+ public void deadParentInAddition() {
+ fullyInitializeRegistry();
+ final TestObserver observer2 = mock(TestObserver.class);
+ final TestObserver observer3 = mock(TestObserver.class);
+
+ TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ mRegistry.removeObserver(this);
+ assertThat(mRegistry.getObserverCount(), is(0));
+ mRegistry.addObserver(observer2);
+ mRegistry.addObserver(observer3);
+ }
+ });
+
+ mRegistry.addObserver(observer1);
+
+ InOrder inOrder = inOrder(observer1, observer2, observer3);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer2).onResume();
+ inOrder.verify(observer3).onStart();
+ inOrder.verify(observer3).onResume();
+ }
+
+ @Test
+ public void deadParentWhileTraversing() {
+ final TestObserver observer2 = mock(TestObserver.class);
+ final TestObserver observer3 = mock(TestObserver.class);
+ TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ mRegistry.removeObserver(this);
+ assertThat(mRegistry.getObserverCount(), is(0));
+ mRegistry.addObserver(observer2);
+ mRegistry.addObserver(observer3);
+ }
+ });
+ InOrder inOrder = inOrder(observer1, observer2, observer3);
+ mRegistry.addObserver(observer1);
+ dispatchEvent(ON_CREATE);
+ dispatchEvent(ON_START);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer3).onStart();
+ }
+
+ @Test
+ public void removeCascade() {
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer4 = mock(TestObserver.class);
+
+ final TestObserver observer2 = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ mRegistry.removeObserver(this);
+ }
+ });
+
+ TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onResume() {
+ mRegistry.removeObserver(this);
+ mRegistry.addObserver(observer2);
+ mRegistry.addObserver(observer3);
+ mRegistry.addObserver(observer4);
+ }
+ });
+ fullyInitializeRegistry();
+ mRegistry.addObserver(observer1);
+ InOrder inOrder = inOrder(observer1, observer2, observer3, observer4);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer1).onResume();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer3).onStart();
+ inOrder.verify(observer4).onCreate();
+ inOrder.verify(observer4).onStart();
+ inOrder.verify(observer3).onResume();
+ inOrder.verify(observer4).onResume();
+ }
+
+ @Test
+ public void changeStateDuringDescending() {
+ final TestObserver observer2 = mock(TestObserver.class);
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onPause() {
+ // but tonight I bounce back
+ mRegistry.handleLifecycleEvent(ON_RESUME);
+ mRegistry.addObserver(observer2);
+ }
+ });
+ fullyInitializeRegistry();
+ mRegistry.addObserver(observer1);
+ mRegistry.handleLifecycleEvent(ON_PAUSE);
+ InOrder inOrder = inOrder(observer1, observer2);
+ inOrder.verify(observer1).onPause();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer1).onResume();
+ inOrder.verify(observer2).onResume();
+ }
+
+ @Test
+ public void siblingLimitationCheck() {
+ fullyInitializeRegistry();
+ final TestObserver observer2 = mock(TestObserver.class);
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ mRegistry.addObserver(observer2);
+ }
+
+ @Override
+ void onResume() {
+ mRegistry.addObserver(observer3);
+ }
+ });
+ mRegistry.addObserver(observer1);
+ InOrder inOrder = inOrder(observer1, observer2, observer3);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer1).onResume();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer2).onStart();
+ inOrder.verify(observer2).onResume();
+ inOrder.verify(observer3).onStart();
+ inOrder.verify(observer3).onResume();
+ }
+
+ @Test
+ public void siblingRemovalLimitationCheck1() {
+ fullyInitializeRegistry();
+ final TestObserver observer2 = mock(TestObserver.class);
+ final TestObserver observer3 = mock(TestObserver.class);
+ final TestObserver observer4 = mock(TestObserver.class);
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ mRegistry.addObserver(observer2);
+ }
+
+ @Override
+ void onResume() {
+ mRegistry.removeObserver(observer2);
+ mRegistry.addObserver(observer3);
+ mRegistry.addObserver(observer4);
+ }
+ });
+ mRegistry.addObserver(observer1);
+ InOrder inOrder = inOrder(observer1, observer2, observer3, observer4);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer1).onResume();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer3).onStart();
+ inOrder.verify(observer4).onCreate();
+ inOrder.verify(observer4).onStart();
+ inOrder.verify(observer3).onResume();
+ inOrder.verify(observer4).onResume();
+ }
+
+ @Test
+ public void siblingRemovalLimitationCheck2() {
+ fullyInitializeRegistry();
+ final TestObserver observer2 = mock(TestObserver.class);
+ final TestObserver observer3 = spy(new TestObserver() {
+ @Override
+ void onCreate() {
+ mRegistry.removeObserver(observer2);
+ }
+ });
+ final TestObserver observer4 = mock(TestObserver.class);
+ final TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ void onStart() {
+ mRegistry.addObserver(observer2);
+ }
+
+ @Override
+ void onResume() {
+ mRegistry.addObserver(observer3);
+ mRegistry.addObserver(observer4);
+ }
+ });
+
+ mRegistry.addObserver(observer1);
+ InOrder inOrder = inOrder(observer1, observer2, observer3, observer4);
+ inOrder.verify(observer1).onCreate();
+ inOrder.verify(observer1).onStart();
+ inOrder.verify(observer2).onCreate();
+ inOrder.verify(observer1).onResume();
+ inOrder.verify(observer3).onCreate();
+ inOrder.verify(observer3).onStart();
+ inOrder.verify(observer4).onCreate();
+ inOrder.verify(observer4).onStart();
+ inOrder.verify(observer3).onResume();
+ inOrder.verify(observer4).onResume();
+ }
+
+ @Test
+ public void sameObserverReAddition() {
+ TestObserver observer = mock(TestObserver.class);
+ mRegistry.addObserver(observer);
+ mRegistry.removeObserver(observer);
+ mRegistry.addObserver(observer);
+ dispatchEvent(ON_CREATE);
+ verify(observer).onCreate();
+ }
+
+ private static void forceGc() {
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ }
+
+ @Test
+ public void goneLifecycleOwner() {
+ fullyInitializeRegistry();
+ mLifecycleOwner = null;
+ forceGc();
+ TestObserver observer = mock(TestObserver.class);
+ mRegistry.addObserver(observer);
+ verify(observer, never()).onCreate();
+ verify(observer, never()).onStart();
+ verify(observer, never()).onResume();
+ }
+
+ private void dispatchEvent(Lifecycle.Event event) {
+ when(mLifecycle.getCurrentState()).thenReturn(LifecycleRegistry.getStateAfter(event));
+ mRegistry.handleLifecycleEvent(event);
+ }
+
+ private void fullyInitializeRegistry() {
+ dispatchEvent(ON_CREATE);
+ dispatchEvent(ON_START);
+ dispatchEvent(ON_RESUME);
+ }
+
+ private abstract class TestObserver implements LifecycleObserver {
+ @OnLifecycleEvent(ON_CREATE)
+ void onCreate() {
+ }
+
+ @OnLifecycleEvent(ON_START)
+ void onStart() {
+ }
+
+ @OnLifecycleEvent(ON_RESUME)
+ void onResume() {
+ }
+
+ @OnLifecycleEvent(ON_PAUSE)
+ void onPause() {
+ }
+
+ @OnLifecycleEvent(ON_STOP)
+ void onStop() {
+ }
+
+ @OnLifecycleEvent(ON_DESTROY)
+ void onDestroy() {
+ }
+ }
+}
diff --git a/lifecycle/viewmodel/api/current.txt b/lifecycle/viewmodel/api/current.txt
index 6a82d4c..802ac19 100644
--- a/lifecycle/viewmodel/api/current.txt
+++ b/lifecycle/viewmodel/api/current.txt
@@ -1,6 +1,6 @@
-package android.arch.lifecycle {
+package androidx.lifecycle {
- public class AndroidViewModel extends android.arch.lifecycle.ViewModel {
+ public class AndroidViewModel extends androidx.lifecycle.ViewModel {
ctor public AndroidViewModel(android.app.Application);
method public <T extends android.app.Application> T getApplication();
}
@@ -11,24 +11,24 @@
}
public class ViewModelProvider {
- ctor public ViewModelProvider(android.arch.lifecycle.ViewModelStoreOwner, android.arch.lifecycle.ViewModelProvider.Factory);
- ctor public ViewModelProvider(android.arch.lifecycle.ViewModelStore, android.arch.lifecycle.ViewModelProvider.Factory);
- method public <T extends android.arch.lifecycle.ViewModel> T get(java.lang.Class<T>);
- method public <T extends android.arch.lifecycle.ViewModel> T get(java.lang.String, java.lang.Class<T>);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStoreOwner, androidx.lifecycle.ViewModelProvider.Factory);
+ ctor public ViewModelProvider(androidx.lifecycle.ViewModelStore, androidx.lifecycle.ViewModelProvider.Factory);
+ method public <T extends androidx.lifecycle.ViewModel> T get(java.lang.Class<T>);
+ method public <T extends androidx.lifecycle.ViewModel> T get(java.lang.String, java.lang.Class<T>);
}
- public static class ViewModelProvider.AndroidViewModelFactory extends android.arch.lifecycle.ViewModelProvider.NewInstanceFactory {
+ public static class ViewModelProvider.AndroidViewModelFactory extends androidx.lifecycle.ViewModelProvider.NewInstanceFactory {
ctor public ViewModelProvider.AndroidViewModelFactory(android.app.Application);
- method public static android.arch.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application);
+ method public static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory getInstance(android.app.Application);
}
public static abstract interface ViewModelProvider.Factory {
- method public abstract <T extends android.arch.lifecycle.ViewModel> T create(java.lang.Class<T>);
+ method public abstract <T extends androidx.lifecycle.ViewModel> T create(java.lang.Class<T>);
}
- public static class ViewModelProvider.NewInstanceFactory implements android.arch.lifecycle.ViewModelProvider.Factory {
+ public static class ViewModelProvider.NewInstanceFactory implements androidx.lifecycle.ViewModelProvider.Factory {
ctor public ViewModelProvider.NewInstanceFactory();
- method public <T extends android.arch.lifecycle.ViewModel> T create(java.lang.Class<T>);
+ method public <T extends androidx.lifecycle.ViewModel> T create(java.lang.Class<T>);
}
public class ViewModelStore {
@@ -37,7 +37,7 @@
}
public abstract interface ViewModelStoreOwner {
- method public abstract android.arch.lifecycle.ViewModelStore getViewModelStore();
+ method public abstract androidx.lifecycle.ViewModelStore getViewModelStore();
}
}
diff --git a/lifecycle/viewmodel/api/1.1.0.txt b/lifecycle/viewmodel/api_legacy/1.1.0.txt
similarity index 100%
rename from lifecycle/viewmodel/api/1.1.0.txt
rename to lifecycle/viewmodel/api_legacy/1.1.0.txt
diff --git a/lifecycle/viewmodel/api/1.1.0.txt b/lifecycle/viewmodel/api_legacy/current.txt
similarity index 100%
copy from lifecycle/viewmodel/api/1.1.0.txt
copy to lifecycle/viewmodel/api_legacy/current.txt
diff --git a/lifecycle/viewmodel/proguard-rules.pro b/lifecycle/viewmodel/proguard-rules.pro
index daac94e..7f40702 100644
--- a/lifecycle/viewmodel/proguard-rules.pro
+++ b/lifecycle/viewmodel/proguard-rules.pro
@@ -1,3 +1,3 @@
--keep class * extends android.arch.lifecycle.ViewModel {
+-keep class * extends androidx.lifecycle.ViewModel {
<init>();
}
\ No newline at end of file
diff --git a/lifecycle/viewmodel/src/main/AndroidManifest.xml b/lifecycle/viewmodel/src/main/AndroidManifest.xml
index c5d9c0d..0fbbcfb 100644
--- a/lifecycle/viewmodel/src/main/AndroidManifest.xml
+++ b/lifecycle/viewmodel/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.lifecycle.viewmodel">
+ package="androidx.lifecycle.viewmodel">
</manifest>
diff --git a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/AndroidViewModel.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/AndroidViewModel.java
deleted file mode 100644
index e8895bd..0000000
--- a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/AndroidViewModel.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.annotation.SuppressLint;
-import android.app.Application;
-import android.support.annotation.NonNull;
-
-/**
- * Application context aware {@link ViewModel}.
- * <p>
- * Subclasses must have a constructor which accepts {@link Application} as the only parameter.
- * <p>
- */
-public class AndroidViewModel extends ViewModel {
- @SuppressLint("StaticFieldLeak")
- private Application mApplication;
-
- public AndroidViewModel(@NonNull Application application) {
- mApplication = application;
- }
-
- /**
- * Return the application.
- */
- @SuppressWarnings("TypeParameterUnusedInFormals")
- @NonNull
- public <T extends Application> T getApplication() {
- //noinspection unchecked
- return (T) mApplication;
- }
-}
diff --git a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModel.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModel.java
deleted file mode 100644
index 0310c46..0000000
--- a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModel.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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;
-
-/**
- * ViewModel is a class that is responsible for preparing and managing the data for
- * an {@link android.app.Activity Activity} or a {@link android.support.v4.app.Fragment Fragment}.
- * It also handles the communication of the Activity / Fragment with the rest of the application
- * (e.g. calling the business logic classes).
- * <p>
- * A ViewModel is always created in association with a scope (an fragment or an activity) and will
- * be retained as long as the scope is alive. E.g. if it is an Activity, until it is
- * finished.
- * <p>
- * In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a
- * configuration change (e.g. rotation). The new instance of the owner will just re-connected to the
- * existing ViewModel.
- * <p>
- * The purpose of the ViewModel is to acquire and keep the information that is necessary for an
- * Activity or a Fragment. The Activity or the Fragment should be able to observe changes in the
- * ViewModel. ViewModels usually expose this information via {@link LiveData} or Android Data
- * Binding. You can also use any observability construct from you favorite framework.
- * <p>
- * ViewModel's only responsibility is to manage the data for the UI. It <b>should never</b> access
- * your view hierarchy or hold a reference back to the Activity or the Fragment.
- * <p>
- * Typical usage from an Activity standpoint would be:
- * <pre>
- * public class UserActivity extends Activity {
- *
- * {@literal @}Override
- * protected void onCreate(Bundle savedInstanceState) {
- * super.onCreate(savedInstanceState);
- * setContentView(R.layout.user_activity_layout);
- * final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);
- * viewModel.userLiveData.observer(this, new Observer<User>() {
- * {@literal @}Override
- * public void onChanged(@Nullable User data) {
- * // update ui.
- * }
- * });
- * findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
- * {@literal @}Override
- * public void onClick(View v) {
- * viewModel.doAction();
- * }
- * });
- * }
- * }
- * </pre>
- *
- * ViewModel would be:
- * <pre>
- * public class UserModel extends ViewModel {
- * public final LiveData<User> userLiveData = new LiveData<>();
- *
- * public UserModel() {
- * // trigger user load.
- * }
- *
- * void doAction() {
- * // depending on the action, do necessary business logic calls and update the
- * // userLiveData.
- * }
- * }
- * </pre>
- *
- * <p>
- * ViewModels can also be used as a communication layer between different Fragments of an Activity.
- * Each Fragment can acquire the ViewModel using the same key via their Activity. This allows
- * communication between Fragments in a de-coupled fashion such that they never need to talk to
- * the other Fragment directly.
- * <pre>
- * public class MyFragment extends Fragment {
- * public void onStart() {
- * UserModel userModel = ViewModelProviders.of(getActivity()).get(UserModel.class);
- * }
- * }
- * </pre>
- * </>
- */
-public abstract class ViewModel {
- /**
- * This method will be called when this ViewModel is no longer used and will be destroyed.
- * <p>
- * It is useful when ViewModel observes some data and you need to clear this subscription to
- * prevent a leak of this ViewModel.
- */
- @SuppressWarnings("WeakerAccess")
- protected void onCleared() {
- }
-}
diff --git a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java
deleted file mode 100644
index 4f25ac9..0000000
--- a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelProvider.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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.app.Application;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-
-import java.lang.reflect.InvocationTargetException;
-
-/**
- * An utility class that provides {@code ViewModels} for a scope.
- * <p>
- * Default {@code ViewModelProvider} for an {@code Activity} or a {@code Fragment} can be obtained
- * from {@link android.arch.lifecycle.ViewModelProviders} class.
- */
-@SuppressWarnings("WeakerAccess")
-public class ViewModelProvider {
-
- private static final String DEFAULT_KEY =
- "android.arch.lifecycle.ViewModelProvider.DefaultKey";
-
- /**
- * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
- */
- public interface Factory {
- /**
- * Creates a new instance of the given {@code Class}.
- * <p>
- *
- * @param modelClass a {@code Class} whose instance is requested
- * @param <T> The type parameter for the ViewModel.
- * @return a newly created ViewModel
- */
- @NonNull
- <T extends ViewModel> T create(@NonNull Class<T> modelClass);
- }
-
- private final Factory mFactory;
- private final ViewModelStore mViewModelStore;
-
- /**
- * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
- * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
- *
- * @param owner a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
- * retain {@code ViewModels}
- * @param factory a {@code Factory} which will be used to instantiate
- * new {@code ViewModels}
- */
- public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
- this(owner.getViewModelStore(), factory);
- }
-
- /**
- * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
- * {@code Factory} and retain them in the given {@code store}.
- *
- * @param store {@code ViewModelStore} where ViewModels will be stored.
- * @param factory factory a {@code Factory} which will be used to instantiate
- * new {@code ViewModels}
- */
- public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
- mFactory = factory;
- this.mViewModelStore = store;
- }
-
- /**
- * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
- * an activity), associated with this {@code ViewModelProvider}.
- * <p>
- * The created ViewModel is associated with the given scope and will be retained
- * as long as the scope is alive (e.g. if it is an activity, until it is
- * finished or process is killed).
- *
- * @param modelClass The class of the ViewModel to create an instance of it if it is not
- * present.
- * @param <T> The type parameter for the ViewModel.
- * @return A ViewModel that is an instance of the given type {@code T}.
- */
- @NonNull
- @MainThread
- public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
- String canonicalName = modelClass.getCanonicalName();
- if (canonicalName == null) {
- throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
- }
- return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
- }
-
- /**
- * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
- * an activity), associated with this {@code ViewModelProvider}.
- * <p>
- * The created ViewModel is associated with the given scope and will be retained
- * as long as the scope is alive (e.g. if it is an activity, until it is
- * finished or process is killed).
- *
- * @param key The key to use to identify the ViewModel.
- * @param modelClass The class of the ViewModel to create an instance of it if it is not
- * present.
- * @param <T> The type parameter for the ViewModel.
- * @return A ViewModel that is an instance of the given type {@code T}.
- */
- @NonNull
- @MainThread
- public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
- ViewModel viewModel = mViewModelStore.get(key);
-
- if (modelClass.isInstance(viewModel)) {
- //noinspection unchecked
- return (T) viewModel;
- } else {
- //noinspection StatementWithEmptyBody
- if (viewModel != null) {
- // TODO: log a warning.
- }
- }
-
- viewModel = mFactory.create(modelClass);
- mViewModelStore.put(key, viewModel);
- //noinspection unchecked
- return (T) viewModel;
- }
-
- /**
- * Simple factory, which calls empty constructor on the give class.
- */
- public static class NewInstanceFactory implements Factory {
-
- @SuppressWarnings("ClassNewInstance")
- @NonNull
- @Override
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- //noinspection TryWithIdenticalCatches
- try {
- return modelClass.newInstance();
- } catch (InstantiationException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- }
- }
- }
-
- /**
- * {@link Factory} which may create {@link AndroidViewModel} and
- * {@link ViewModel}, which have an empty constructor.
- */
- public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
-
- private static AndroidViewModelFactory sInstance;
-
- /**
- * Retrieve a singleton instance of AndroidViewModelFactory.
- *
- * @param application an application to pass in {@link AndroidViewModel}
- * @return A valid {@link AndroidViewModelFactory}
- */
- @NonNull
- public static AndroidViewModelFactory getInstance(@NonNull Application application) {
- if (sInstance == null) {
- sInstance = new AndroidViewModelFactory(application);
- }
- return sInstance;
- }
-
- private Application mApplication;
-
- /**
- * Creates a {@code AndroidViewModelFactory}
- *
- * @param application an application to pass in {@link AndroidViewModel}
- */
- public AndroidViewModelFactory(@NonNull Application application) {
- mApplication = application;
- }
-
- @NonNull
- @Override
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
- //noinspection TryWithIdenticalCatches
- try {
- return modelClass.getConstructor(Application.class).newInstance(mApplication);
- } catch (NoSuchMethodException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InstantiationException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- }
- }
- return super.create(modelClass);
- }
- }
-}
diff --git a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelStore.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelStore.java
deleted file mode 100644
index 74b5e24..0000000
--- a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelStore.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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 java.util.HashMap;
-
-/**
- * Class to store {@code ViewModels}.
- * <p>
- * An instance of {@code ViewModelStore} must be retained through configuration changes:
- * if an owner of this {@code ViewModelStore} is destroyed and recreated due to configuration
- * changes, new instance of an owner should still have the same old instance of
- * {@code ViewModelStore}.
- * <p>
- * If an owner of this {@code ViewModelStore} is destroyed and is not going to be recreated,
- * then it should call {@link #clear()} on this {@code ViewModelStore}, so {@code ViewModels} would
- * be notified that they are no longer used.
- * <p>
- * {@link android.arch.lifecycle.ViewModelStores} provides a {@code ViewModelStore} for
- * activities and fragments.
- */
-public class ViewModelStore {
-
- private final HashMap<String, ViewModel> mMap = new HashMap<>();
-
- final void put(String key, ViewModel viewModel) {
- ViewModel oldViewModel = mMap.put(key, viewModel);
- if (oldViewModel != null) {
- oldViewModel.onCleared();
- }
- }
-
- final ViewModel get(String key) {
- return mMap.get(key);
- }
-
- /**
- * Clears internal storage and notifies ViewModels that they are no longer used.
- */
- public final void clear() {
- for (ViewModel vm : mMap.values()) {
- vm.onCleared();
- }
- mMap.clear();
- }
-}
diff --git a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelStoreOwner.java b/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelStoreOwner.java
deleted file mode 100644
index e26fa32..0000000
--- a/lifecycle/viewmodel/src/main/java/android/arch/lifecycle/ViewModelStoreOwner.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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;
-
-/**
- * A scope that owns {@link ViewModelStore}.
- * <p>
- * A responsibility of an implementation of this interface is to retain owned ViewModelStore
- * during the configuration changes and call {@link ViewModelStore#clear()}, when this scope is
- * going to be destroyed.
- */
-@SuppressWarnings("WeakerAccess")
-public interface ViewModelStoreOwner {
- /**
- * Returns owned {@link ViewModelStore}
- *
- * @return a {@code ViewModelStore}
- */
- @NonNull
- ViewModelStore getViewModelStore();
-}
diff --git a/lifecycle/viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.java b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.java
new file mode 100644
index 0000000..1782453
--- /dev/null
+++ b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.java
@@ -0,0 +1,47 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Application context aware {@link ViewModel}.
+ * <p>
+ * Subclasses must have a constructor which accepts {@link Application} as the only parameter.
+ * <p>
+ */
+public class AndroidViewModel extends ViewModel {
+ @SuppressLint("StaticFieldLeak")
+ private Application mApplication;
+
+ public AndroidViewModel(@NonNull Application application) {
+ mApplication = application;
+ }
+
+ /**
+ * Return the application.
+ */
+ @SuppressWarnings("TypeParameterUnusedInFormals")
+ @NonNull
+ public <T extends Application> T getApplication() {
+ //noinspection unchecked
+ return (T) mApplication;
+ }
+}
diff --git a/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
new file mode 100644
index 0000000..ef9ce7f
--- /dev/null
+++ b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -0,0 +1,106 @@
+/*
+ * 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 androidx.lifecycle;
+
+/**
+ * ViewModel is a class that is responsible for preparing and managing the data for
+ * an {@link android.app.Activity Activity} or a {@link androidx.fragment.app.Fragment Fragment}.
+ * It also handles the communication of the Activity / Fragment with the rest of the application
+ * (e.g. calling the business logic classes).
+ * <p>
+ * A ViewModel is always created in association with a scope (an fragment or an activity) and will
+ * be retained as long as the scope is alive. E.g. if it is an Activity, until it is
+ * finished.
+ * <p>
+ * In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a
+ * configuration change (e.g. rotation). The new instance of the owner will just re-connected to the
+ * existing ViewModel.
+ * <p>
+ * The purpose of the ViewModel is to acquire and keep the information that is necessary for an
+ * Activity or a Fragment. The Activity or the Fragment should be able to observe changes in the
+ * ViewModel. ViewModels usually expose this information via {@link LiveData} or Android Data
+ * Binding. You can also use any observability construct from you favorite framework.
+ * <p>
+ * ViewModel's only responsibility is to manage the data for the UI. It <b>should never</b> access
+ * your view hierarchy or hold a reference back to the Activity or the Fragment.
+ * <p>
+ * Typical usage from an Activity standpoint would be:
+ * <pre>
+ * public class UserActivity extends Activity {
+ *
+ * {@literal @}Override
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * setContentView(R.layout.user_activity_layout);
+ * final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);
+ * viewModel.userLiveData.observer(this, new Observer<User>() {
+ * {@literal @}Override
+ * public void onChanged(@Nullable User data) {
+ * // update ui.
+ * }
+ * });
+ * findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
+ * {@literal @}Override
+ * public void onClick(View v) {
+ * viewModel.doAction();
+ * }
+ * });
+ * }
+ * }
+ * </pre>
+ *
+ * ViewModel would be:
+ * <pre>
+ * public class UserModel extends ViewModel {
+ * public final LiveData<User> userLiveData = new LiveData<>();
+ *
+ * public UserModel() {
+ * // trigger user load.
+ * }
+ *
+ * void doAction() {
+ * // depending on the action, do necessary business logic calls and update the
+ * // userLiveData.
+ * }
+ * }
+ * </pre>
+ *
+ * <p>
+ * ViewModels can also be used as a communication layer between different Fragments of an Activity.
+ * Each Fragment can acquire the ViewModel using the same key via their Activity. This allows
+ * communication between Fragments in a de-coupled fashion such that they never need to talk to
+ * the other Fragment directly.
+ * <pre>
+ * public class MyFragment extends Fragment {
+ * public void onStart() {
+ * UserModel userModel = ViewModelProviders.of(getActivity()).get(UserModel.class);
+ * }
+ * }
+ * </pre>
+ * </>
+ */
+public abstract class ViewModel {
+ /**
+ * This method will be called when this ViewModel is no longer used and will be destroyed.
+ * <p>
+ * It is useful when ViewModel observes some data and you need to clear this subscription to
+ * prevent a leak of this ViewModel.
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected void onCleared() {
+ }
+}
diff --git a/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.java b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.java
new file mode 100644
index 0000000..8e3ba9b
--- /dev/null
+++ b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelProvider.java
@@ -0,0 +1,214 @@
+/*
+ * 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 androidx.lifecycle;
+
+import android.app.Application;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * An utility class that provides {@code ViewModels} for a scope.
+ * <p>
+ * Default {@code ViewModelProvider} for an {@code Activity} or a {@code Fragment} can be obtained
+ * from {@link androidx.lifecycle.ViewModelProviders} class.
+ */
+@SuppressWarnings("WeakerAccess")
+public class ViewModelProvider {
+
+ private static final String DEFAULT_KEY =
+ "androidx.lifecycle.ViewModelProvider.DefaultKey";
+
+ /**
+ * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
+ */
+ public interface Factory {
+ /**
+ * Creates a new instance of the given {@code Class}.
+ * <p>
+ *
+ * @param modelClass a {@code Class} whose instance is requested
+ * @param <T> The type parameter for the ViewModel.
+ * @return a newly created ViewModel
+ */
+ @NonNull
+ <T extends ViewModel> T create(@NonNull Class<T> modelClass);
+ }
+
+ private final Factory mFactory;
+ private final ViewModelStore mViewModelStore;
+
+ /**
+ * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
+ * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
+ *
+ * @param owner a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
+ * retain {@code ViewModels}
+ * @param factory a {@code Factory} which will be used to instantiate
+ * new {@code ViewModels}
+ */
+ public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
+ this(owner.getViewModelStore(), factory);
+ }
+
+ /**
+ * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
+ * {@code Factory} and retain them in the given {@code store}.
+ *
+ * @param store {@code ViewModelStore} where ViewModels will be stored.
+ * @param factory factory a {@code Factory} which will be used to instantiate
+ * new {@code ViewModels}
+ */
+ public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
+ mFactory = factory;
+ this.mViewModelStore = store;
+ }
+
+ /**
+ * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
+ * an activity), associated with this {@code ViewModelProvider}.
+ * <p>
+ * The created ViewModel is associated with the given scope and will be retained
+ * as long as the scope is alive (e.g. if it is an activity, until it is
+ * finished or process is killed).
+ *
+ * @param modelClass The class of the ViewModel to create an instance of it if it is not
+ * present.
+ * @param <T> The type parameter for the ViewModel.
+ * @return A ViewModel that is an instance of the given type {@code T}.
+ */
+ @NonNull
+ @MainThread
+ public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
+ String canonicalName = modelClass.getCanonicalName();
+ if (canonicalName == null) {
+ throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
+ }
+ return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
+ }
+
+ /**
+ * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
+ * an activity), associated with this {@code ViewModelProvider}.
+ * <p>
+ * The created ViewModel is associated with the given scope and will be retained
+ * as long as the scope is alive (e.g. if it is an activity, until it is
+ * finished or process is killed).
+ *
+ * @param key The key to use to identify the ViewModel.
+ * @param modelClass The class of the ViewModel to create an instance of it if it is not
+ * present.
+ * @param <T> The type parameter for the ViewModel.
+ * @return A ViewModel that is an instance of the given type {@code T}.
+ */
+ @NonNull
+ @MainThread
+ public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
+ ViewModel viewModel = mViewModelStore.get(key);
+
+ if (modelClass.isInstance(viewModel)) {
+ //noinspection unchecked
+ return (T) viewModel;
+ } else {
+ //noinspection StatementWithEmptyBody
+ if (viewModel != null) {
+ // TODO: log a warning.
+ }
+ }
+
+ viewModel = mFactory.create(modelClass);
+ mViewModelStore.put(key, viewModel);
+ //noinspection unchecked
+ return (T) viewModel;
+ }
+
+ /**
+ * Simple factory, which calls empty constructor on the give class.
+ */
+ public static class NewInstanceFactory implements Factory {
+
+ @SuppressWarnings("ClassNewInstance")
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ return modelClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ }
+ }
+ }
+
+ /**
+ * {@link Factory} which may create {@link AndroidViewModel} and
+ * {@link ViewModel}, which have an empty constructor.
+ */
+ public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
+
+ private static AndroidViewModelFactory sInstance;
+
+ /**
+ * Retrieve a singleton instance of AndroidViewModelFactory.
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ * @return A valid {@link AndroidViewModelFactory}
+ */
+ @NonNull
+ public static AndroidViewModelFactory getInstance(@NonNull Application application) {
+ if (sInstance == null) {
+ sInstance = new AndroidViewModelFactory(application);
+ }
+ return sInstance;
+ }
+
+ private Application mApplication;
+
+ /**
+ * Creates a {@code AndroidViewModelFactory}
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ */
+ public AndroidViewModelFactory(@NonNull Application application) {
+ mApplication = application;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ return modelClass.getConstructor(Application.class).newInstance(mApplication);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ }
+ }
+ return super.create(modelClass);
+ }
+ }
+}
diff --git a/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelStore.java b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelStore.java
new file mode 100644
index 0000000..c831563
--- /dev/null
+++ b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelStore.java
@@ -0,0 +1,60 @@
+/*
+ * 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 androidx.lifecycle;
+
+import java.util.HashMap;
+
+/**
+ * Class to store {@code ViewModels}.
+ * <p>
+ * An instance of {@code ViewModelStore} must be retained through configuration changes:
+ * if an owner of this {@code ViewModelStore} is destroyed and recreated due to configuration
+ * changes, new instance of an owner should still have the same old instance of
+ * {@code ViewModelStore}.
+ * <p>
+ * If an owner of this {@code ViewModelStore} is destroyed and is not going to be recreated,
+ * then it should call {@link #clear()} on this {@code ViewModelStore}, so {@code ViewModels} would
+ * be notified that they are no longer used.
+ * <p>
+ * {@link androidx.lifecycle.ViewModelStores} provides a {@code ViewModelStore} for
+ * activities and fragments.
+ */
+public class ViewModelStore {
+
+ private final HashMap<String, ViewModel> mMap = new HashMap<>();
+
+ final void put(String key, ViewModel viewModel) {
+ ViewModel oldViewModel = mMap.put(key, viewModel);
+ if (oldViewModel != null) {
+ oldViewModel.onCleared();
+ }
+ }
+
+ final ViewModel get(String key) {
+ return mMap.get(key);
+ }
+
+ /**
+ * Clears internal storage and notifies ViewModels that they are no longer used.
+ */
+ public final void clear() {
+ for (ViewModel vm : mMap.values()) {
+ vm.onCleared();
+ }
+ mMap.clear();
+ }
+}
diff --git a/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java
new file mode 100644
index 0000000..d9bec9f
--- /dev/null
+++ b/lifecycle/viewmodel/src/main/java/androidx/lifecycle/ViewModelStoreOwner.java
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.lifecycle;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A scope that owns {@link ViewModelStore}.
+ * <p>
+ * A responsibility of an implementation of this interface is to retain owned ViewModelStore
+ * during the configuration changes and call {@link ViewModelStore#clear()}, when this scope is
+ * going to be destroyed.
+ */
+@SuppressWarnings("WeakerAccess")
+public interface ViewModelStoreOwner {
+ /**
+ * Returns owned {@link ViewModelStore}
+ *
+ * @return a {@code ViewModelStore}
+ */
+ @NonNull
+ ViewModelStore getViewModelStore();
+}
diff --git a/lifecycle/viewmodel/src/test/java/android/arch/lifecycle/ViewModelProviderTest.java b/lifecycle/viewmodel/src/test/java/android/arch/lifecycle/ViewModelProviderTest.java
deleted file mode 100644
index 142f19a..0000000
--- a/lifecycle/viewmodel/src/test/java/android/arch/lifecycle/ViewModelProviderTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 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.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory;
-import android.support.annotation.NonNull;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class ViewModelProviderTest {
-
- private ViewModelProvider mViewModelProvider;
-
- @Before
- public void setup() {
- mViewModelProvider = new ViewModelProvider(new ViewModelStore(), new NewInstanceFactory());
- }
-
- @Test
- public void twoViewModelsWithSameKey() throws Throwable {
- String key = "the_key";
- ViewModel1 vm1 = mViewModelProvider.get(key, ViewModel1.class);
- assertThat(vm1.mCleared, is(false));
- ViewModel2 vw2 = mViewModelProvider.get(key, ViewModel2.class);
- assertThat(vw2, notNullValue());
- assertThat(vm1.mCleared, is(true));
- }
-
-
- @Test
- public void localViewModel() throws Throwable {
- class VM extends ViewModel1 {
- }
- try {
- mViewModelProvider.get(VM.class);
- Assert.fail();
- } catch (IllegalArgumentException ignored) {
- }
- }
-
- @Test
- public void twoViewModels() {
- ViewModel1 model1 = mViewModelProvider.get(ViewModel1.class);
- ViewModel2 model2 = mViewModelProvider.get(ViewModel2.class);
- assertThat(mViewModelProvider.get(ViewModel1.class), is(model1));
- assertThat(mViewModelProvider.get(ViewModel2.class), is(model2));
- }
-
- @Test
- public void testOwnedBy() {
- final ViewModelStore store = new ViewModelStore();
- ViewModelStoreOwner owner = new ViewModelStoreOwner() {
- @NonNull
- @Override
- public ViewModelStore getViewModelStore() {
- return store;
- }
- };
- ViewModelProvider provider = new ViewModelProvider(owner, new NewInstanceFactory());
- ViewModel1 viewModel = provider.get(ViewModel1.class);
- assertThat(viewModel, is(provider.get(ViewModel1.class)));
- }
-
- public static class ViewModel1 extends ViewModel {
- boolean mCleared;
-
- @Override
- protected void onCleared() {
- mCleared = true;
- }
- }
-
- public static class ViewModel2 extends ViewModel {
- }
-}
diff --git a/lifecycle/viewmodel/src/test/java/android/arch/lifecycle/ViewModelStoreTest.java b/lifecycle/viewmodel/src/test/java/android/arch/lifecycle/ViewModelStoreTest.java
deleted file mode 100644
index cfeefb7..0000000
--- a/lifecycle/viewmodel/src/test/java/android/arch/lifecycle/ViewModelStoreTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class ViewModelStoreTest {
-
- @Test
- public void testClear() {
- ViewModelStore store = new ViewModelStore();
- TestViewModel viewModel1 = new TestViewModel();
- TestViewModel viewModel2 = new TestViewModel();
- store.put("a", viewModel1);
- store.put("b", viewModel2);
- assertThat(viewModel1.mCleared, is(false));
- assertThat(viewModel2.mCleared, is(false));
- store.clear();
- assertThat(viewModel1.mCleared, is(true));
- assertThat(viewModel2.mCleared, is(true));
- assertThat(store.get("a"), nullValue());
- assertThat(store.get("b"), nullValue());
- }
-
- static class TestViewModel extends ViewModel {
- boolean mCleared = false;
-
- @Override
- protected void onCleared() {
- mCleared = true;
- }
- }
-}
diff --git a/lifecycle/viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.java b/lifecycle/viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.java
new file mode 100644
index 0000000..e63c3e4
--- /dev/null
+++ b/lifecycle/viewmodel/src/test/java/androidx/lifecycle/ViewModelProviderTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider.NewInstanceFactory;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ViewModelProviderTest {
+
+ private ViewModelProvider mViewModelProvider;
+
+ @Before
+ public void setup() {
+ mViewModelProvider = new ViewModelProvider(new ViewModelStore(), new NewInstanceFactory());
+ }
+
+ @Test
+ public void twoViewModelsWithSameKey() throws Throwable {
+ String key = "the_key";
+ ViewModel1 vm1 = mViewModelProvider.get(key, ViewModel1.class);
+ assertThat(vm1.mCleared, is(false));
+ ViewModel2 vw2 = mViewModelProvider.get(key, ViewModel2.class);
+ assertThat(vw2, notNullValue());
+ assertThat(vm1.mCleared, is(true));
+ }
+
+
+ @Test
+ public void localViewModel() throws Throwable {
+ class VM extends ViewModel1 {
+ }
+ try {
+ mViewModelProvider.get(VM.class);
+ Assert.fail();
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+
+ @Test
+ public void twoViewModels() {
+ ViewModel1 model1 = mViewModelProvider.get(ViewModel1.class);
+ ViewModel2 model2 = mViewModelProvider.get(ViewModel2.class);
+ assertThat(mViewModelProvider.get(ViewModel1.class), is(model1));
+ assertThat(mViewModelProvider.get(ViewModel2.class), is(model2));
+ }
+
+ @Test
+ public void testOwnedBy() {
+ final ViewModelStore store = new ViewModelStore();
+ ViewModelStoreOwner owner = new ViewModelStoreOwner() {
+ @NonNull
+ @Override
+ public ViewModelStore getViewModelStore() {
+ return store;
+ }
+ };
+ ViewModelProvider provider = new ViewModelProvider(owner, new NewInstanceFactory());
+ ViewModel1 viewModel = provider.get(ViewModel1.class);
+ assertThat(viewModel, is(provider.get(ViewModel1.class)));
+ }
+
+ public static class ViewModel1 extends ViewModel {
+ boolean mCleared;
+
+ @Override
+ protected void onCleared() {
+ mCleared = true;
+ }
+ }
+
+ public static class ViewModel2 extends ViewModel {
+ }
+}
diff --git a/lifecycle/viewmodel/src/test/java/androidx/lifecycle/ViewModelStoreTest.java b/lifecycle/viewmodel/src/test/java/androidx/lifecycle/ViewModelStoreTest.java
new file mode 100644
index 0000000..0805b76
--- /dev/null
+++ b/lifecycle/viewmodel/src/test/java/androidx/lifecycle/ViewModelStoreTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 androidx.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ViewModelStoreTest {
+
+ @Test
+ public void testClear() {
+ ViewModelStore store = new ViewModelStore();
+ TestViewModel viewModel1 = new TestViewModel();
+ TestViewModel viewModel2 = new TestViewModel();
+ store.put("a", viewModel1);
+ store.put("b", viewModel2);
+ assertThat(viewModel1.mCleared, is(false));
+ assertThat(viewModel2.mCleared, is(false));
+ store.clear();
+ assertThat(viewModel1.mCleared, is(true));
+ assertThat(viewModel2.mCleared, is(true));
+ assertThat(store.get("a"), nullValue());
+ assertThat(store.get("b"), nullValue());
+ }
+
+ static class TestViewModel extends ViewModel {
+ boolean mCleared = false;
+
+ @Override
+ protected void onCleared() {
+ mCleared = true;
+ }
+ }
+}
diff --git a/loader/src/androidTest/java/androidx/loader/app/LoaderInfoTest.java b/loader/src/androidTest/java/androidx/loader/app/LoaderInfoTest.java
index 510553a..ecdf44f 100644
--- a/loader/src/androidTest/java/androidx/loader/app/LoaderInfoTest.java
+++ b/loader/src/androidTest/java/androidx/loader/app/LoaderInfoTest.java
@@ -21,15 +21,15 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.LifecycleRegistry;
import android.content.Context;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
import androidx.loader.app.test.DelayLoader;
import androidx.loader.app.test.DummyLoaderCallbacks;
import androidx.loader.app.test.EmptyActivity;
@@ -74,7 +74,12 @@
assertFalse("isCallbackWaitingForData should be false before setCallback",
loaderInfo.isCallbackWaitingForData());
- loaderInfo.setCallback(mActivityRule.getActivity(), loaderCallback);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ loaderInfo.setCallback(mActivityRule.getActivity(), loaderCallback);
+ }
+ });
assertTrue("isCallbackWaitingForData should be true immediately after setCallback",
loaderInfo.isCallbackWaitingForData());
diff --git a/loader/src/androidTest/java/androidx/loader/app/LoaderManagerTest.java b/loader/src/androidTest/java/androidx/loader/app/LoaderManagerTest.java
index 8ac2dcf..cd60ee8 100644
--- a/loader/src/androidTest/java/androidx/loader/app/LoaderManagerTest.java
+++ b/loader/src/androidTest/java/androidx/loader/app/LoaderManagerTest.java
@@ -21,11 +21,6 @@
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.LifecycleRegistry;
-import android.arch.lifecycle.ViewModelStore;
-import android.arch.lifecycle.ViewModelStoreOwner;
import android.content.Context;
import android.os.Bundle;
import android.support.test.filters.SmallTest;
@@ -33,6 +28,11 @@
import android.support.test.runner.AndroidJUnit4;
import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.ViewModelStore;
+import androidx.lifecycle.ViewModelStoreOwner;
import androidx.loader.app.test.DelayLoaderCallbacks;
import androidx.loader.app.test.DummyLoaderCallbacks;
import androidx.loader.app.test.EmptyActivity;
diff --git a/loader/src/main/java/androidx/loader/app/LoaderManager.java b/loader/src/main/java/androidx/loader/app/LoaderManager.java
index 764de74..8897575 100644
--- a/loader/src/main/java/androidx/loader/app/LoaderManager.java
+++ b/loader/src/main/java/androidx/loader/app/LoaderManager.java
@@ -16,13 +16,13 @@
package androidx.loader.app;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.ViewModelStoreOwner;
import android.os.Bundle;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.ViewModelStoreOwner;
import androidx.loader.content.Loader;
import java.io.FileDescriptor;
diff --git a/loader/src/main/java/androidx/loader/app/LoaderManagerImpl.java b/loader/src/main/java/androidx/loader/app/LoaderManagerImpl.java
index e1f2eee..770779d 100644
--- a/loader/src/main/java/androidx/loader/app/LoaderManagerImpl.java
+++ b/loader/src/main/java/androidx/loader/app/LoaderManagerImpl.java
@@ -16,12 +16,6 @@
package androidx.loader.app;
-import android.arch.lifecycle.LifecycleOwner;
-import android.arch.lifecycle.MutableLiveData;
-import android.arch.lifecycle.Observer;
-import android.arch.lifecycle.ViewModel;
-import android.arch.lifecycle.ViewModelProvider;
-import android.arch.lifecycle.ViewModelStore;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
@@ -31,6 +25,12 @@
import androidx.annotation.Nullable;
import androidx.collection.SparseArrayCompat;
import androidx.core.util.DebugUtils;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStore;
import androidx.loader.content.Loader;
import java.io.FileDescriptor;
@@ -130,7 +130,7 @@
}
@Override
- public void removeObserver(@NonNull Observer<D> observer) {
+ public void removeObserver(@NonNull Observer<? super D> observer) {
super.removeObserver(observer);
// Clear out our references when the observer is removed to avoid leaking
mLifecycleOwner = null;
diff --git a/media/api/current.txt b/media/api/current.txt
index 36577fa..d7df744 100644
--- a/media/api/current.txt
+++ b/media/api/current.txt
@@ -1,5 +1,79 @@
package android.support.v4.media {
+ public final class MediaBrowserCompat {
+ ctor public MediaBrowserCompat(android.content.Context, android.content.ComponentName, android.support.v4.media.MediaBrowserCompat.ConnectionCallback, android.os.Bundle);
+ method public void connect();
+ method public void disconnect();
+ method public android.os.Bundle getExtras();
+ method public void getItem(java.lang.String, android.support.v4.media.MediaBrowserCompat.ItemCallback);
+ method public java.lang.String getRoot();
+ method public android.content.ComponentName getServiceComponent();
+ method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
+ method public boolean isConnected();
+ method public void search(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SearchCallback);
+ method public void sendCustomAction(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.CustomActionCallback);
+ method public void subscribe(java.lang.String, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
+ method public void subscribe(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
+ method public void unsubscribe(java.lang.String);
+ method public void unsubscribe(java.lang.String, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
+ field public static final java.lang.String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD";
+ field public static final java.lang.String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE = "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE";
+ field public static final java.lang.String EXTRA_DOWNLOAD_PROGRESS = "android.media.browse.extra.DOWNLOAD_PROGRESS";
+ field public static final java.lang.String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID";
+ field public static final java.lang.String EXTRA_PAGE = "android.media.browse.extra.PAGE";
+ field public static final java.lang.String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
+ }
+
+ public static class MediaBrowserCompat.ConnectionCallback {
+ ctor public MediaBrowserCompat.ConnectionCallback();
+ method public void onConnected();
+ method public void onConnectionFailed();
+ method public void onConnectionSuspended();
+ }
+
+ public static abstract class MediaBrowserCompat.CustomActionCallback {
+ ctor public MediaBrowserCompat.CustomActionCallback();
+ method public void onError(java.lang.String, android.os.Bundle, android.os.Bundle);
+ method public void onProgressUpdate(java.lang.String, android.os.Bundle, android.os.Bundle);
+ method public void onResult(java.lang.String, android.os.Bundle, android.os.Bundle);
+ }
+
+ public static abstract class MediaBrowserCompat.ItemCallback {
+ ctor public MediaBrowserCompat.ItemCallback();
+ method public void onError(java.lang.String);
+ method public void onItemLoaded(android.support.v4.media.MediaBrowserCompat.MediaItem);
+ }
+
+ public static class MediaBrowserCompat.MediaItem implements android.os.Parcelable {
+ ctor public MediaBrowserCompat.MediaItem(android.support.v4.media.MediaDescriptionCompat, int);
+ method public int describeContents();
+ method public static android.support.v4.media.MediaBrowserCompat.MediaItem fromMediaItem(java.lang.Object);
+ method public static java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem> fromMediaItemList(java.util.List<?>);
+ method public android.support.v4.media.MediaDescriptionCompat getDescription();
+ method public int getFlags();
+ method public java.lang.String getMediaId();
+ method public boolean isBrowsable();
+ method public boolean isPlayable();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.support.v4.media.MediaBrowserCompat.MediaItem> CREATOR;
+ field public static final int FLAG_BROWSABLE = 1; // 0x1
+ field public static final int FLAG_PLAYABLE = 2; // 0x2
+ }
+
+ public static abstract class MediaBrowserCompat.SearchCallback {
+ ctor public MediaBrowserCompat.SearchCallback();
+ method public void onError(java.lang.String, android.os.Bundle);
+ method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+ }
+
+ public static abstract class MediaBrowserCompat.SubscriptionCallback {
+ ctor public MediaBrowserCompat.SubscriptionCallback();
+ method public void onChildrenLoaded(java.lang.String, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+ method public void onChildrenLoaded(java.lang.String, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>, android.os.Bundle);
+ method public void onError(java.lang.String);
+ method public void onError(java.lang.String, android.os.Bundle);
+ }
+
public final class MediaDescriptionCompat implements android.os.Parcelable {
method public int describeContents();
method public static android.support.v4.media.MediaDescriptionCompat fromMediaDescription(java.lang.Object);
@@ -131,12 +205,101 @@
package android.support.v4.media.session {
+ public final class MediaControllerCompat {
+ ctor public MediaControllerCompat(android.content.Context, android.support.v4.media.session.MediaSessionCompat);
+ ctor public MediaControllerCompat(android.content.Context, android.support.v4.media.session.MediaSessionCompat.Token) throws android.os.RemoteException;
+ method public void addQueueItem(android.support.v4.media.MediaDescriptionCompat);
+ method public void addQueueItem(android.support.v4.media.MediaDescriptionCompat, int);
+ method public void adjustVolume(int, int);
+ method public boolean dispatchMediaButtonEvent(android.view.KeyEvent);
+ method public android.os.Bundle getExtras();
+ method public long getFlags();
+ method public static android.support.v4.media.session.MediaControllerCompat getMediaController(android.app.Activity);
+ method public java.lang.Object getMediaController();
+ method public android.support.v4.media.MediaMetadataCompat getMetadata();
+ method public java.lang.String getPackageName();
+ method public android.support.v4.media.session.MediaControllerCompat.PlaybackInfo getPlaybackInfo();
+ method public android.support.v4.media.session.PlaybackStateCompat getPlaybackState();
+ method public java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem> getQueue();
+ method public java.lang.CharSequence getQueueTitle();
+ method public int getRatingType();
+ method public int getRepeatMode();
+ method public android.app.PendingIntent getSessionActivity();
+ method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
+ method public int getShuffleMode();
+ method public android.support.v4.media.session.MediaControllerCompat.TransportControls getTransportControls();
+ method public boolean isCaptioningEnabled();
+ method public boolean isSessionReady();
+ method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback);
+ method public void registerCallback(android.support.v4.media.session.MediaControllerCompat.Callback, android.os.Handler);
+ method public void removeQueueItem(android.support.v4.media.MediaDescriptionCompat);
+ method public deprecated void removeQueueItemAt(int);
+ method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+ method public static void setMediaController(android.app.Activity, android.support.v4.media.session.MediaControllerCompat);
+ method public void setVolumeTo(int, int);
+ method public void unregisterCallback(android.support.v4.media.session.MediaControllerCompat.Callback);
+ }
+
+ public static abstract class MediaControllerCompat.Callback implements android.os.IBinder.DeathRecipient {
+ ctor public MediaControllerCompat.Callback();
+ method public void binderDied();
+ method public void onAudioInfoChanged(android.support.v4.media.session.MediaControllerCompat.PlaybackInfo);
+ method public void onCaptioningEnabledChanged(boolean);
+ method public void onExtrasChanged(android.os.Bundle);
+ method public void onMetadataChanged(android.support.v4.media.MediaMetadataCompat);
+ method public void onPlaybackStateChanged(android.support.v4.media.session.PlaybackStateCompat);
+ method public void onQueueChanged(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
+ method public void onQueueTitleChanged(java.lang.CharSequence);
+ method public void onRepeatModeChanged(int);
+ method public void onSessionDestroyed();
+ method public void onSessionEvent(java.lang.String, android.os.Bundle);
+ method public void onSessionReady();
+ method public void onShuffleModeChanged(int);
+ }
+
+ public static final class MediaControllerCompat.PlaybackInfo {
+ method public int getAudioStream();
+ method public int getCurrentVolume();
+ method public int getMaxVolume();
+ method public int getPlaybackType();
+ method public int getVolumeControl();
+ field public static final int PLAYBACK_TYPE_LOCAL = 1; // 0x1
+ field public static final int PLAYBACK_TYPE_REMOTE = 2; // 0x2
+ }
+
+ public static abstract class MediaControllerCompat.TransportControls {
+ method public abstract void fastForward();
+ method public abstract void pause();
+ method public abstract void play();
+ method public abstract void playFromMediaId(java.lang.String, android.os.Bundle);
+ method public abstract void playFromSearch(java.lang.String, android.os.Bundle);
+ method public abstract void playFromUri(android.net.Uri, android.os.Bundle);
+ method public abstract void prepare();
+ method public abstract void prepareFromMediaId(java.lang.String, android.os.Bundle);
+ method public abstract void prepareFromSearch(java.lang.String, android.os.Bundle);
+ method public abstract void prepareFromUri(android.net.Uri, android.os.Bundle);
+ method public abstract void rewind();
+ method public abstract void seekTo(long);
+ method public abstract void sendCustomAction(android.support.v4.media.session.PlaybackStateCompat.CustomAction, android.os.Bundle);
+ method public abstract void sendCustomAction(java.lang.String, android.os.Bundle);
+ method public abstract void setCaptioningEnabled(boolean);
+ method public abstract void setRating(android.support.v4.media.RatingCompat);
+ method public abstract void setRating(android.support.v4.media.RatingCompat, android.os.Bundle);
+ method public abstract void setRepeatMode(int);
+ method public abstract void setShuffleMode(int);
+ method public abstract void skipToNext();
+ method public abstract void skipToPrevious();
+ method public abstract void skipToQueueItem(long);
+ method public abstract void stop();
+ field public static final java.lang.String EXTRA_LEGACY_STREAM_TYPE = "android.media.session.extra.LEGACY_STREAM_TYPE";
+ }
+
public class MediaSessionCompat {
ctor public MediaSessionCompat(android.content.Context, java.lang.String);
ctor public MediaSessionCompat(android.content.Context, java.lang.String, android.content.ComponentName, android.app.PendingIntent);
method public void addOnActiveChangeListener(android.support.v4.media.session.MediaSessionCompat.OnActiveChangeListener);
method public static android.support.v4.media.session.MediaSessionCompat fromMediaSession(android.content.Context, java.lang.Object);
- method public androidx.media.session.MediaControllerCompat getController();
+ method public android.support.v4.media.session.MediaControllerCompat getController();
method public java.lang.Object getMediaSession();
method public java.lang.Object getRemoteControlClient();
method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
@@ -402,80 +565,6 @@
method public androidx.media.AudioAttributesCompat.Builder setUsage(int);
}
- public final class MediaBrowserCompat {
- ctor public MediaBrowserCompat(android.content.Context, android.content.ComponentName, androidx.media.MediaBrowserCompat.ConnectionCallback, android.os.Bundle);
- method public void connect();
- method public void disconnect();
- method public android.os.Bundle getExtras();
- method public void getItem(java.lang.String, androidx.media.MediaBrowserCompat.ItemCallback);
- method public java.lang.String getRoot();
- method public android.content.ComponentName getServiceComponent();
- method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
- method public boolean isConnected();
- method public void search(java.lang.String, android.os.Bundle, androidx.media.MediaBrowserCompat.SearchCallback);
- method public void sendCustomAction(java.lang.String, android.os.Bundle, androidx.media.MediaBrowserCompat.CustomActionCallback);
- method public void subscribe(java.lang.String, androidx.media.MediaBrowserCompat.SubscriptionCallback);
- method public void subscribe(java.lang.String, android.os.Bundle, androidx.media.MediaBrowserCompat.SubscriptionCallback);
- method public void unsubscribe(java.lang.String);
- method public void unsubscribe(java.lang.String, androidx.media.MediaBrowserCompat.SubscriptionCallback);
- field public static final java.lang.String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD";
- field public static final java.lang.String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE = "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE";
- field public static final java.lang.String EXTRA_DOWNLOAD_PROGRESS = "android.media.browse.extra.DOWNLOAD_PROGRESS";
- field public static final java.lang.String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID";
- field public static final java.lang.String EXTRA_PAGE = "android.media.browse.extra.PAGE";
- field public static final java.lang.String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
- }
-
- public static class MediaBrowserCompat.ConnectionCallback {
- ctor public MediaBrowserCompat.ConnectionCallback();
- method public void onConnected();
- method public void onConnectionFailed();
- method public void onConnectionSuspended();
- }
-
- public static abstract class MediaBrowserCompat.CustomActionCallback {
- ctor public MediaBrowserCompat.CustomActionCallback();
- method public void onError(java.lang.String, android.os.Bundle, android.os.Bundle);
- method public void onProgressUpdate(java.lang.String, android.os.Bundle, android.os.Bundle);
- method public void onResult(java.lang.String, android.os.Bundle, android.os.Bundle);
- }
-
- public static abstract class MediaBrowserCompat.ItemCallback {
- ctor public MediaBrowserCompat.ItemCallback();
- method public void onError(java.lang.String);
- method public void onItemLoaded(androidx.media.MediaBrowserCompat.MediaItem);
- }
-
- public static class MediaBrowserCompat.MediaItem implements android.os.Parcelable {
- ctor public MediaBrowserCompat.MediaItem(android.support.v4.media.MediaDescriptionCompat, int);
- method public int describeContents();
- method public static androidx.media.MediaBrowserCompat.MediaItem fromMediaItem(java.lang.Object);
- method public static java.util.List<androidx.media.MediaBrowserCompat.MediaItem> fromMediaItemList(java.util.List<?>);
- method public android.support.v4.media.MediaDescriptionCompat getDescription();
- method public int getFlags();
- method public java.lang.String getMediaId();
- method public boolean isBrowsable();
- method public boolean isPlayable();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.media.MediaBrowserCompat.MediaItem> CREATOR;
- field public static final int FLAG_BROWSABLE = 1; // 0x1
- field public static final int FLAG_PLAYABLE = 2; // 0x2
- }
-
- public static abstract class MediaBrowserCompat.SearchCallback {
- ctor public MediaBrowserCompat.SearchCallback();
- method public void onError(java.lang.String, android.os.Bundle);
- method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List<androidx.media.MediaBrowserCompat.MediaItem>);
- }
-
- public static abstract class MediaBrowserCompat.SubscriptionCallback {
- ctor public MediaBrowserCompat.SubscriptionCallback();
- method public void onChildrenLoaded(java.lang.String, java.util.List<androidx.media.MediaBrowserCompat.MediaItem>);
- method public void onChildrenLoaded(java.lang.String, java.util.List<androidx.media.MediaBrowserCompat.MediaItem>, android.os.Bundle);
- method public void onError(java.lang.String);
- method public void onError(java.lang.String, android.os.Bundle);
- }
-
public abstract class MediaBrowserServiceCompat extends android.app.Service {
ctor public MediaBrowserServiceCompat();
method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
@@ -486,10 +575,10 @@
method public android.os.IBinder onBind(android.content.Intent);
method public void onCustomAction(java.lang.String, android.os.Bundle, androidx.media.MediaBrowserServiceCompat.Result<android.os.Bundle>);
method public abstract androidx.media.MediaBrowserServiceCompat.BrowserRoot onGetRoot(java.lang.String, int, android.os.Bundle);
- method public abstract void onLoadChildren(java.lang.String, androidx.media.MediaBrowserServiceCompat.Result<java.util.List<androidx.media.MediaBrowserCompat.MediaItem>>);
- method public void onLoadChildren(java.lang.String, androidx.media.MediaBrowserServiceCompat.Result<java.util.List<androidx.media.MediaBrowserCompat.MediaItem>>, android.os.Bundle);
- method public void onLoadItem(java.lang.String, androidx.media.MediaBrowserServiceCompat.Result<androidx.media.MediaBrowserCompat.MediaItem>);
- method public void onSearch(java.lang.String, android.os.Bundle, androidx.media.MediaBrowserServiceCompat.Result<java.util.List<androidx.media.MediaBrowserCompat.MediaItem>>);
+ method public abstract void onLoadChildren(java.lang.String, androidx.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
+ method public void onLoadChildren(java.lang.String, androidx.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>, android.os.Bundle);
+ method public void onLoadItem(java.lang.String, androidx.media.MediaBrowserServiceCompat.Result<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+ method public void onSearch(java.lang.String, android.os.Bundle, androidx.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
method public void setSessionToken(android.support.v4.media.session.MediaSessionCompat.Token);
field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
}
@@ -564,94 +653,5 @@
method public void onReceive(android.content.Context, android.content.Intent);
}
- public final class MediaControllerCompat {
- ctor public MediaControllerCompat(android.content.Context, android.support.v4.media.session.MediaSessionCompat);
- ctor public MediaControllerCompat(android.content.Context, android.support.v4.media.session.MediaSessionCompat.Token) throws android.os.RemoteException;
- method public void addQueueItem(android.support.v4.media.MediaDescriptionCompat);
- method public void addQueueItem(android.support.v4.media.MediaDescriptionCompat, int);
- method public void adjustVolume(int, int);
- method public boolean dispatchMediaButtonEvent(android.view.KeyEvent);
- method public android.os.Bundle getExtras();
- method public long getFlags();
- method public static androidx.media.session.MediaControllerCompat getMediaController(android.app.Activity);
- method public java.lang.Object getMediaController();
- method public android.support.v4.media.MediaMetadataCompat getMetadata();
- method public java.lang.String getPackageName();
- method public androidx.media.session.MediaControllerCompat.PlaybackInfo getPlaybackInfo();
- method public android.support.v4.media.session.PlaybackStateCompat getPlaybackState();
- method public java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem> getQueue();
- method public java.lang.CharSequence getQueueTitle();
- method public int getRatingType();
- method public int getRepeatMode();
- method public android.app.PendingIntent getSessionActivity();
- method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
- method public int getShuffleMode();
- method public androidx.media.session.MediaControllerCompat.TransportControls getTransportControls();
- method public boolean isCaptioningEnabled();
- method public boolean isSessionReady();
- method public void registerCallback(androidx.media.session.MediaControllerCompat.Callback);
- method public void registerCallback(androidx.media.session.MediaControllerCompat.Callback, android.os.Handler);
- method public void removeQueueItem(android.support.v4.media.MediaDescriptionCompat);
- method public deprecated void removeQueueItemAt(int);
- method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
- method public static void setMediaController(android.app.Activity, androidx.media.session.MediaControllerCompat);
- method public void setVolumeTo(int, int);
- method public void unregisterCallback(androidx.media.session.MediaControllerCompat.Callback);
- }
-
- public static abstract class MediaControllerCompat.Callback implements android.os.IBinder.DeathRecipient {
- ctor public MediaControllerCompat.Callback();
- method public void binderDied();
- method public void onAudioInfoChanged(androidx.media.session.MediaControllerCompat.PlaybackInfo);
- method public void onCaptioningEnabledChanged(boolean);
- method public void onExtrasChanged(android.os.Bundle);
- method public void onMetadataChanged(android.support.v4.media.MediaMetadataCompat);
- method public void onPlaybackStateChanged(android.support.v4.media.session.PlaybackStateCompat);
- method public void onQueueChanged(java.util.List<android.support.v4.media.session.MediaSessionCompat.QueueItem>);
- method public void onQueueTitleChanged(java.lang.CharSequence);
- method public void onRepeatModeChanged(int);
- method public void onSessionDestroyed();
- method public void onSessionEvent(java.lang.String, android.os.Bundle);
- method public void onSessionReady();
- method public void onShuffleModeChanged(int);
- }
-
- public static final class MediaControllerCompat.PlaybackInfo {
- method public int getAudioStream();
- method public int getCurrentVolume();
- method public int getMaxVolume();
- method public int getPlaybackType();
- method public int getVolumeControl();
- field public static final int PLAYBACK_TYPE_LOCAL = 1; // 0x1
- field public static final int PLAYBACK_TYPE_REMOTE = 2; // 0x2
- }
-
- public static abstract class MediaControllerCompat.TransportControls {
- method public abstract void fastForward();
- method public abstract void pause();
- method public abstract void play();
- method public abstract void playFromMediaId(java.lang.String, android.os.Bundle);
- method public abstract void playFromSearch(java.lang.String, android.os.Bundle);
- method public abstract void playFromUri(android.net.Uri, android.os.Bundle);
- method public abstract void prepare();
- method public abstract void prepareFromMediaId(java.lang.String, android.os.Bundle);
- method public abstract void prepareFromSearch(java.lang.String, android.os.Bundle);
- method public abstract void prepareFromUri(android.net.Uri, android.os.Bundle);
- method public abstract void rewind();
- method public abstract void seekTo(long);
- method public abstract void sendCustomAction(android.support.v4.media.session.PlaybackStateCompat.CustomAction, android.os.Bundle);
- method public abstract void sendCustomAction(java.lang.String, android.os.Bundle);
- method public abstract void setCaptioningEnabled(boolean);
- method public abstract void setRating(android.support.v4.media.RatingCompat);
- method public abstract void setRating(android.support.v4.media.RatingCompat, android.os.Bundle);
- method public abstract void setRepeatMode(int);
- method public abstract void setShuffleMode(int);
- method public abstract void skipToNext();
- method public abstract void skipToPrevious();
- method public abstract void skipToQueueItem(long);
- method public abstract void stop();
- field public static final java.lang.String EXTRA_LEGACY_STREAM_TYPE = "android.media.session.extra.LEGACY_STREAM_TYPE";
- }
-
}
diff --git a/media/api21/android/support/v4/media/MediaBrowserCompatApi21.java b/media/api21/android/support/v4/media/MediaBrowserCompatApi21.java
new file mode 100644
index 0000000..4f040df
--- /dev/null
+++ b/media/api21/android/support/v4/media/MediaBrowserCompatApi21.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018 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.support.v4.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.browse.MediaBrowser;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.List;
+
+@RequiresApi(21)
+class MediaBrowserCompatApi21 {
+ static final String NULL_MEDIA_ITEM_ID =
+ "android.support.v4.media.MediaBrowserCompat.NULL_MEDIA_ITEM";
+
+ public static Object createConnectionCallback(ConnectionCallback callback) {
+ return new ConnectionCallbackProxy<>(callback);
+ }
+
+ public static Object createBrowser(Context context, ComponentName serviceComponent,
+ Object callback, Bundle rootHints) {
+ return new MediaBrowser(context, serviceComponent,
+ (MediaBrowser.ConnectionCallback) callback, rootHints);
+ }
+
+ public static void connect(Object browserObj) {
+ ((MediaBrowser)browserObj).connect();
+ }
+
+ public static void disconnect(Object browserObj) {
+ ((MediaBrowser)browserObj).disconnect();
+
+ }
+
+ public static boolean isConnected(Object browserObj) {
+ return ((MediaBrowser)browserObj).isConnected();
+ }
+
+ public static ComponentName getServiceComponent(Object browserObj) {
+ return ((MediaBrowser)browserObj).getServiceComponent();
+ }
+
+ public static String getRoot(Object browserObj) {
+ return ((MediaBrowser)browserObj).getRoot();
+ }
+
+ public static Bundle getExtras(Object browserObj) {
+ return ((MediaBrowser)browserObj).getExtras();
+ }
+
+ public static Object getSessionToken(Object browserObj) {
+ return ((MediaBrowser)browserObj).getSessionToken();
+ }
+
+ public static Object createSubscriptionCallback(SubscriptionCallback callback) {
+ return new SubscriptionCallbackProxy<>(callback);
+ }
+
+ public static void subscribe(
+ Object browserObj, String parentId, Object subscriptionCallbackObj) {
+ ((MediaBrowser)browserObj).subscribe(parentId,
+ (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
+ }
+
+ public static void unsubscribe(Object browserObj, String parentId) {
+ ((MediaBrowser)browserObj).unsubscribe(parentId);
+ }
+
+ interface ConnectionCallback {
+ void onConnected();
+ void onConnectionSuspended();
+ void onConnectionFailed();
+ }
+
+ static class ConnectionCallbackProxy<T extends ConnectionCallback>
+ extends MediaBrowser.ConnectionCallback {
+ protected final T mConnectionCallback;
+
+ public ConnectionCallbackProxy(T connectionCallback) {
+ mConnectionCallback = connectionCallback;
+ }
+
+ @Override
+ public void onConnected() {
+ mConnectionCallback.onConnected();
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ mConnectionCallback.onConnectionSuspended();
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ mConnectionCallback.onConnectionFailed();
+ }
+ }
+
+ interface SubscriptionCallback {
+ void onChildrenLoaded(@NonNull String parentId, List<?> children);
+ void onError(@NonNull String parentId);
+ }
+
+ static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
+ extends MediaBrowser.SubscriptionCallback {
+ protected final T mSubscriptionCallback;
+
+ public SubscriptionCallbackProxy(T callback) {
+ mSubscriptionCallback = callback;
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId,
+ List<MediaBrowser.MediaItem> children) {
+ mSubscriptionCallback.onChildrenLoaded(parentId, children);
+ }
+
+ @Override
+ public void onError(@NonNull String parentId) {
+ mSubscriptionCallback.onError(parentId);
+ }
+ }
+
+ static class MediaItem {
+
+ public static int getFlags(Object itemObj) {
+ return ((MediaBrowser.MediaItem) itemObj).getFlags();
+ }
+
+ public static Object getDescription(Object itemObj) {
+ return ((MediaBrowser.MediaItem) itemObj).getDescription();
+ }
+ }
+}
diff --git a/media/api21/android/support/v4/media/session/MediaControllerCompatApi21.java b/media/api21/android/support/v4/media/session/MediaControllerCompatApi21.java
new file mode 100644
index 0000000..6c5f860
--- /dev/null
+++ b/media/api21/android/support/v4/media/session/MediaControllerCompatApi21.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2018 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.support.v4.media.session;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.Rating;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.view.KeyEvent;
+
+import androidx.annotation.RequiresApi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(21)
+class MediaControllerCompatApi21 {
+ public static Object fromToken(Context context, Object sessionToken) {
+ return new MediaController(context, (MediaSession.Token) sessionToken);
+ }
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static void registerCallback(Object controllerObj, Object callbackObj, Handler handler) {
+ ((MediaController) controllerObj).registerCallback(
+ (MediaController.Callback)callbackObj, handler);
+ }
+
+ public static void unregisterCallback(Object controllerObj, Object callbackObj) {
+ ((MediaController) controllerObj)
+ .unregisterCallback((MediaController.Callback) callbackObj);
+ }
+
+ public static void setMediaController(Activity activity, Object controllerObj) {
+ activity.setMediaController((MediaController) controllerObj);
+ }
+
+ public static Object getMediaController(Activity activity) {
+ return activity.getMediaController();
+ }
+
+ public static Object getSessionToken(Object controllerObj) {
+ return ((MediaController) controllerObj).getSessionToken();
+ }
+
+ public static Object getTransportControls(Object controllerObj) {
+ return ((MediaController)controllerObj).getTransportControls();
+ }
+
+ public static Object getPlaybackState(Object controllerObj) {
+ return ((MediaController)controllerObj).getPlaybackState();
+ }
+
+ public static Object getMetadata(Object controllerObj) {
+ return ((MediaController)controllerObj).getMetadata();
+ }
+
+ public static List<Object> getQueue(Object controllerObj) {
+ List<MediaSession.QueueItem> queue = ((MediaController) controllerObj).getQueue();
+ if (queue == null) {
+ return null;
+ }
+ List<Object> queueObjs = new ArrayList<Object>(queue);
+ return queueObjs;
+ }
+
+ public static CharSequence getQueueTitle(Object controllerObj) {
+ return ((MediaController) controllerObj).getQueueTitle();
+ }
+
+ public static Bundle getExtras(Object controllerObj) {
+ return ((MediaController) controllerObj).getExtras();
+ }
+
+ public static int getRatingType(Object controllerObj) {
+ return ((MediaController) controllerObj).getRatingType();
+ }
+
+ public static long getFlags(Object controllerObj) {
+ return ((MediaController) controllerObj).getFlags();
+ }
+
+ public static Object getPlaybackInfo(Object controllerObj) {
+ return ((MediaController) controllerObj).getPlaybackInfo();
+ }
+
+ public static PendingIntent getSessionActivity(Object controllerObj) {
+ return ((MediaController) controllerObj).getSessionActivity();
+ }
+
+ public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) {
+ return ((MediaController) controllerObj).dispatchMediaButtonEvent(event);
+ }
+
+ public static void setVolumeTo(Object controllerObj, int value, int flags) {
+ ((MediaController) controllerObj).setVolumeTo(value, flags);
+ }
+
+ public static void adjustVolume(Object controllerObj, int direction, int flags) {
+ ((MediaController) controllerObj).adjustVolume(direction, flags);
+ }
+
+ public static void sendCommand(Object controllerObj,
+ String command, Bundle params, ResultReceiver cb) {
+ ((MediaController) controllerObj).sendCommand(command, params, cb);
+ }
+
+ public static String getPackageName(Object controllerObj) {
+ return ((MediaController) controllerObj).getPackageName();
+ }
+
+ public static class TransportControls {
+ public static void play(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).play();
+ }
+
+ public static void pause(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).pause();
+ }
+
+ public static void stop(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).stop();
+ }
+
+ public static void seekTo(Object controlsObj, long pos) {
+ ((MediaController.TransportControls)controlsObj).seekTo(pos);
+ }
+
+ public static void fastForward(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).fastForward();
+ }
+
+ public static void rewind(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).rewind();
+ }
+
+ public static void skipToNext(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).skipToNext();
+ }
+
+ public static void skipToPrevious(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).skipToPrevious();
+ }
+
+ public static void setRating(Object controlsObj, Object ratingObj) {
+ ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj);
+ }
+
+ public static void playFromMediaId(Object controlsObj, String mediaId, Bundle extras) {
+ ((MediaController.TransportControls) controlsObj).playFromMediaId(mediaId, extras);
+ }
+
+ public static void playFromSearch(Object controlsObj, String query, Bundle extras) {
+ ((MediaController.TransportControls) controlsObj).playFromSearch(query, extras);
+ }
+
+ public static void skipToQueueItem(Object controlsObj, long id) {
+ ((MediaController.TransportControls) controlsObj).skipToQueueItem(id);
+ }
+
+ public static void sendCustomAction(Object controlsObj, String action, Bundle args) {
+ ((MediaController.TransportControls) controlsObj).sendCustomAction(action, args);
+ }
+ }
+
+ public static class PlaybackInfo {
+ public static int getPlaybackType(Object volumeInfoObj) {
+ return ((MediaController.PlaybackInfo)volumeInfoObj).getPlaybackType();
+ }
+
+ public static AudioAttributes getAudioAttributes(Object volumeInfoObj) {
+ return ((MediaController.PlaybackInfo) volumeInfoObj).getAudioAttributes();
+ }
+
+ public static int getLegacyAudioStream(Object volumeInfoObj) {
+ AudioAttributes attrs = getAudioAttributes(volumeInfoObj);
+ return toLegacyStreamType(attrs);
+ }
+
+ public static int getVolumeControl(Object volumeInfoObj) {
+ return ((MediaController.PlaybackInfo)volumeInfoObj).getVolumeControl();
+ }
+
+ public static int getMaxVolume(Object volumeInfoObj) {
+ return ((MediaController.PlaybackInfo)volumeInfoObj).getMaxVolume();
+ }
+
+ public static int getCurrentVolume(Object volumeInfoObj) {
+ return ((MediaController.PlaybackInfo)volumeInfoObj).getCurrentVolume();
+ }
+
+ // This is copied from AudioAttributes.toLegacyStreamType. TODO This
+ // either needs to be kept in sync with that one or toLegacyStreamType
+ // needs to be made public so it can be used by the support lib.
+ private static final int FLAG_SCO = 0x1 << 2;
+ private static final int STREAM_BLUETOOTH_SCO = 6;
+ private static final int STREAM_SYSTEM_ENFORCED = 7;
+ private static int toLegacyStreamType(AudioAttributes aa) {
+ // flags to stream type mapping
+ if ((aa.getFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ == AudioAttributes.FLAG_AUDIBILITY_ENFORCED) {
+ return STREAM_SYSTEM_ENFORCED;
+ }
+ if ((aa.getFlags() & FLAG_SCO) == FLAG_SCO) {
+ return STREAM_BLUETOOTH_SCO;
+ }
+
+ // usage to stream type mapping
+ switch (aa.getUsage()) {
+ case AudioAttributes.USAGE_MEDIA:
+ case AudioAttributes.USAGE_GAME:
+ case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY:
+ case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+ return AudioManager.STREAM_MUSIC;
+ case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION:
+ return AudioManager.STREAM_SYSTEM;
+ case AudioAttributes.USAGE_VOICE_COMMUNICATION:
+ return AudioManager.STREAM_VOICE_CALL;
+ case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+ return AudioManager.STREAM_DTMF;
+ case AudioAttributes.USAGE_ALARM:
+ return AudioManager.STREAM_ALARM;
+ case AudioAttributes.USAGE_NOTIFICATION_RINGTONE:
+ return AudioManager.STREAM_RING;
+ case AudioAttributes.USAGE_NOTIFICATION:
+ case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+ case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+ case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+ case AudioAttributes.USAGE_NOTIFICATION_EVENT:
+ return AudioManager.STREAM_NOTIFICATION;
+ case AudioAttributes.USAGE_UNKNOWN:
+ default:
+ return AudioManager.STREAM_MUSIC;
+ }
+ }
+ }
+
+ public static interface Callback {
+ public void onSessionDestroyed();
+ public void onSessionEvent(String event, Bundle extras);
+ public void onPlaybackStateChanged(Object stateObj);
+ public void onMetadataChanged(Object metadataObj);
+ public void onQueueChanged(List<?> queue);
+ public void onQueueTitleChanged(CharSequence title);
+ public void onExtrasChanged(Bundle extras);
+ public void onAudioInfoChanged(int type, int stream, int control, int max, int current);
+ }
+
+ static class CallbackProxy<T extends Callback> extends MediaController.Callback {
+ protected final T mCallback;
+
+ public CallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ mCallback.onSessionDestroyed();
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ mCallback.onSessionEvent(event, extras);
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ mCallback.onPlaybackStateChanged(state);
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ mCallback.onMetadataChanged(metadata);
+ }
+
+ @Override
+ public void onQueueChanged(List<MediaSession.QueueItem> queue) {
+ mCallback.onQueueChanged(queue);
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) {
+ mCallback.onQueueTitleChanged(title);
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ mCallback.onExtrasChanged(extras);
+ }
+
+ @Override
+ public void onAudioInfoChanged(MediaController.PlaybackInfo info){
+ mCallback.onAudioInfoChanged(info.getPlaybackType(),
+ PlaybackInfo.getLegacyAudioStream(info), info.getVolumeControl(),
+ info.getMaxVolume(), info.getCurrentVolume());
+ }
+ }
+}
diff --git a/media/api21/androidx/media/MediaBrowserCompatApi21.java b/media/api21/androidx/media/MediaBrowserCompatApi21.java
deleted file mode 100644
index 842f54a..0000000
--- a/media/api21/androidx/media/MediaBrowserCompatApi21.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import java.util.List;
-
-@RequiresApi(21)
-class MediaBrowserCompatApi21 {
- static final String NULL_MEDIA_ITEM_ID =
- "androidx.media.MediaBrowserCompat.NULL_MEDIA_ITEM";
-
- public static Object createConnectionCallback(ConnectionCallback callback) {
- return new ConnectionCallbackProxy<>(callback);
- }
-
- public static Object createBrowser(Context context, ComponentName serviceComponent,
- Object callback, Bundle rootHints) {
- return new MediaBrowser(context, serviceComponent,
- (MediaBrowser.ConnectionCallback) callback, rootHints);
- }
-
- public static void connect(Object browserObj) {
- ((MediaBrowser)browserObj).connect();
- }
-
- public static void disconnect(Object browserObj) {
- ((MediaBrowser)browserObj).disconnect();
-
- }
-
- public static boolean isConnected(Object browserObj) {
- return ((MediaBrowser)browserObj).isConnected();
- }
-
- public static ComponentName getServiceComponent(Object browserObj) {
- return ((MediaBrowser)browserObj).getServiceComponent();
- }
-
- public static String getRoot(Object browserObj) {
- return ((MediaBrowser)browserObj).getRoot();
- }
-
- public static Bundle getExtras(Object browserObj) {
- return ((MediaBrowser)browserObj).getExtras();
- }
-
- public static Object getSessionToken(Object browserObj) {
- return ((MediaBrowser)browserObj).getSessionToken();
- }
-
- public static Object createSubscriptionCallback(SubscriptionCallback callback) {
- return new SubscriptionCallbackProxy<>(callback);
- }
-
- public static void subscribe(
- Object browserObj, String parentId, Object subscriptionCallbackObj) {
- ((MediaBrowser)browserObj).subscribe(parentId,
- (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
- }
-
- public static void unsubscribe(Object browserObj, String parentId) {
- ((MediaBrowser)browserObj).unsubscribe(parentId);
- }
-
- interface ConnectionCallback {
- void onConnected();
- void onConnectionSuspended();
- void onConnectionFailed();
- }
-
- static class ConnectionCallbackProxy<T extends ConnectionCallback>
- extends MediaBrowser.ConnectionCallback {
- protected final T mConnectionCallback;
-
- public ConnectionCallbackProxy(T connectionCallback) {
- mConnectionCallback = connectionCallback;
- }
-
- @Override
- public void onConnected() {
- mConnectionCallback.onConnected();
- }
-
- @Override
- public void onConnectionSuspended() {
- mConnectionCallback.onConnectionSuspended();
- }
-
- @Override
- public void onConnectionFailed() {
- mConnectionCallback.onConnectionFailed();
- }
- }
-
- interface SubscriptionCallback {
- void onChildrenLoaded(@NonNull String parentId, List<?> children);
- void onError(@NonNull String parentId);
- }
-
- static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
- extends MediaBrowser.SubscriptionCallback {
- protected final T mSubscriptionCallback;
-
- public SubscriptionCallbackProxy(T callback) {
- mSubscriptionCallback = callback;
- }
-
- @Override
- public void onChildrenLoaded(@NonNull String parentId,
- List<MediaBrowser.MediaItem> children) {
- mSubscriptionCallback.onChildrenLoaded(parentId, children);
- }
-
- @Override
- public void onError(@NonNull String parentId) {
- mSubscriptionCallback.onError(parentId);
- }
- }
-
- static class MediaItem {
-
- public static int getFlags(Object itemObj) {
- return ((MediaBrowser.MediaItem) itemObj).getFlags();
- }
-
- public static Object getDescription(Object itemObj) {
- return ((MediaBrowser.MediaItem) itemObj).getDescription();
- }
- }
-}
diff --git a/media/api21/androidx/media/session/MediaControllerCompatApi21.java b/media/api21/androidx/media/session/MediaControllerCompatApi21.java
deleted file mode 100644
index 9da2b83..0000000
--- a/media/api21/androidx/media/session/MediaControllerCompatApi21.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media.session;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.MediaMetadata;
-import android.media.Rating;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.ResultReceiver;
-import android.view.KeyEvent;
-
-import androidx.annotation.RequiresApi;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(21)
-class MediaControllerCompatApi21 {
- public static Object fromToken(Context context, Object sessionToken) {
- return new MediaController(context, (MediaSession.Token) sessionToken);
- }
-
- public static Object createCallback(Callback callback) {
- return new CallbackProxy<Callback>(callback);
- }
-
- public static void registerCallback(Object controllerObj, Object callbackObj, Handler handler) {
- ((MediaController) controllerObj).registerCallback(
- (MediaController.Callback)callbackObj, handler);
- }
-
- public static void unregisterCallback(Object controllerObj, Object callbackObj) {
- ((MediaController) controllerObj)
- .unregisterCallback((MediaController.Callback) callbackObj);
- }
-
- public static void setMediaController(Activity activity, Object controllerObj) {
- activity.setMediaController((MediaController) controllerObj);
- }
-
- public static Object getMediaController(Activity activity) {
- return activity.getMediaController();
- }
-
- public static Object getSessionToken(Object controllerObj) {
- return ((MediaController) controllerObj).getSessionToken();
- }
-
- public static Object getTransportControls(Object controllerObj) {
- return ((MediaController)controllerObj).getTransportControls();
- }
-
- public static Object getPlaybackState(Object controllerObj) {
- return ((MediaController)controllerObj).getPlaybackState();
- }
-
- public static Object getMetadata(Object controllerObj) {
- return ((MediaController)controllerObj).getMetadata();
- }
-
- public static List<Object> getQueue(Object controllerObj) {
- List<MediaSession.QueueItem> queue = ((MediaController) controllerObj).getQueue();
- if (queue == null) {
- return null;
- }
- List<Object> queueObjs = new ArrayList<Object>(queue);
- return queueObjs;
- }
-
- public static CharSequence getQueueTitle(Object controllerObj) {
- return ((MediaController) controllerObj).getQueueTitle();
- }
-
- public static Bundle getExtras(Object controllerObj) {
- return ((MediaController) controllerObj).getExtras();
- }
-
- public static int getRatingType(Object controllerObj) {
- return ((MediaController) controllerObj).getRatingType();
- }
-
- public static long getFlags(Object controllerObj) {
- return ((MediaController) controllerObj).getFlags();
- }
-
- public static Object getPlaybackInfo(Object controllerObj) {
- return ((MediaController) controllerObj).getPlaybackInfo();
- }
-
- public static PendingIntent getSessionActivity(Object controllerObj) {
- return ((MediaController) controllerObj).getSessionActivity();
- }
-
- public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) {
- return ((MediaController) controllerObj).dispatchMediaButtonEvent(event);
- }
-
- public static void setVolumeTo(Object controllerObj, int value, int flags) {
- ((MediaController) controllerObj).setVolumeTo(value, flags);
- }
-
- public static void adjustVolume(Object controllerObj, int direction, int flags) {
- ((MediaController) controllerObj).adjustVolume(direction, flags);
- }
-
- public static void sendCommand(Object controllerObj,
- String command, Bundle params, ResultReceiver cb) {
- ((MediaController) controllerObj).sendCommand(command, params, cb);
- }
-
- public static String getPackageName(Object controllerObj) {
- return ((MediaController) controllerObj).getPackageName();
- }
-
- public static class TransportControls {
- public static void play(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).play();
- }
-
- public static void pause(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).pause();
- }
-
- public static void stop(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).stop();
- }
-
- public static void seekTo(Object controlsObj, long pos) {
- ((MediaController.TransportControls)controlsObj).seekTo(pos);
- }
-
- public static void fastForward(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).fastForward();
- }
-
- public static void rewind(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).rewind();
- }
-
- public static void skipToNext(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).skipToNext();
- }
-
- public static void skipToPrevious(Object controlsObj) {
- ((MediaController.TransportControls)controlsObj).skipToPrevious();
- }
-
- public static void setRating(Object controlsObj, Object ratingObj) {
- ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj);
- }
-
- public static void playFromMediaId(Object controlsObj, String mediaId, Bundle extras) {
- ((MediaController.TransportControls) controlsObj).playFromMediaId(mediaId, extras);
- }
-
- public static void playFromSearch(Object controlsObj, String query, Bundle extras) {
- ((MediaController.TransportControls) controlsObj).playFromSearch(query, extras);
- }
-
- public static void skipToQueueItem(Object controlsObj, long id) {
- ((MediaController.TransportControls) controlsObj).skipToQueueItem(id);
- }
-
- public static void sendCustomAction(Object controlsObj, String action, Bundle args) {
- ((MediaController.TransportControls) controlsObj).sendCustomAction(action, args);
- }
- }
-
- public static class PlaybackInfo {
- public static int getPlaybackType(Object volumeInfoObj) {
- return ((MediaController.PlaybackInfo)volumeInfoObj).getPlaybackType();
- }
-
- public static AudioAttributes getAudioAttributes(Object volumeInfoObj) {
- return ((MediaController.PlaybackInfo) volumeInfoObj).getAudioAttributes();
- }
-
- public static int getLegacyAudioStream(Object volumeInfoObj) {
- AudioAttributes attrs = getAudioAttributes(volumeInfoObj);
- return toLegacyStreamType(attrs);
- }
-
- public static int getVolumeControl(Object volumeInfoObj) {
- return ((MediaController.PlaybackInfo)volumeInfoObj).getVolumeControl();
- }
-
- public static int getMaxVolume(Object volumeInfoObj) {
- return ((MediaController.PlaybackInfo)volumeInfoObj).getMaxVolume();
- }
-
- public static int getCurrentVolume(Object volumeInfoObj) {
- return ((MediaController.PlaybackInfo)volumeInfoObj).getCurrentVolume();
- }
-
- // This is copied from AudioAttributes.toLegacyStreamType. TODO This
- // either needs to be kept in sync with that one or toLegacyStreamType
- // needs to be made public so it can be used by the support lib.
- private static final int FLAG_SCO = 0x1 << 2;
- private static final int STREAM_BLUETOOTH_SCO = 6;
- private static final int STREAM_SYSTEM_ENFORCED = 7;
- private static int toLegacyStreamType(AudioAttributes aa) {
- // flags to stream type mapping
- if ((aa.getFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
- == AudioAttributes.FLAG_AUDIBILITY_ENFORCED) {
- return STREAM_SYSTEM_ENFORCED;
- }
- if ((aa.getFlags() & FLAG_SCO) == FLAG_SCO) {
- return STREAM_BLUETOOTH_SCO;
- }
-
- // usage to stream type mapping
- switch (aa.getUsage()) {
- case AudioAttributes.USAGE_MEDIA:
- case AudioAttributes.USAGE_GAME:
- case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY:
- case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
- return AudioManager.STREAM_MUSIC;
- case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION:
- return AudioManager.STREAM_SYSTEM;
- case AudioAttributes.USAGE_VOICE_COMMUNICATION:
- return AudioManager.STREAM_VOICE_CALL;
- case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING:
- return AudioManager.STREAM_DTMF;
- case AudioAttributes.USAGE_ALARM:
- return AudioManager.STREAM_ALARM;
- case AudioAttributes.USAGE_NOTIFICATION_RINGTONE:
- return AudioManager.STREAM_RING;
- case AudioAttributes.USAGE_NOTIFICATION:
- case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
- case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
- case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
- case AudioAttributes.USAGE_NOTIFICATION_EVENT:
- return AudioManager.STREAM_NOTIFICATION;
- case AudioAttributes.USAGE_UNKNOWN:
- default:
- return AudioManager.STREAM_MUSIC;
- }
- }
- }
-
- public static interface Callback {
- public void onSessionDestroyed();
- public void onSessionEvent(String event, Bundle extras);
- public void onPlaybackStateChanged(Object stateObj);
- public void onMetadataChanged(Object metadataObj);
- public void onQueueChanged(List<?> queue);
- public void onQueueTitleChanged(CharSequence title);
- public void onExtrasChanged(Bundle extras);
- public void onAudioInfoChanged(int type, int stream, int control, int max, int current);
- }
-
- static class CallbackProxy<T extends Callback> extends MediaController.Callback {
- protected final T mCallback;
-
- public CallbackProxy(T callback) {
- mCallback = callback;
- }
-
- @Override
- public void onSessionDestroyed() {
- mCallback.onSessionDestroyed();
- }
-
- @Override
- public void onSessionEvent(String event, Bundle extras) {
- mCallback.onSessionEvent(event, extras);
- }
-
- @Override
- public void onPlaybackStateChanged(PlaybackState state) {
- mCallback.onPlaybackStateChanged(state);
- }
-
- @Override
- public void onMetadataChanged(MediaMetadata metadata) {
- mCallback.onMetadataChanged(metadata);
- }
-
- @Override
- public void onQueueChanged(List<MediaSession.QueueItem> queue) {
- mCallback.onQueueChanged(queue);
- }
-
- @Override
- public void onQueueTitleChanged(CharSequence title) {
- mCallback.onQueueTitleChanged(title);
- }
-
- @Override
- public void onExtrasChanged(Bundle extras) {
- mCallback.onExtrasChanged(extras);
- }
-
- @Override
- public void onAudioInfoChanged(MediaController.PlaybackInfo info){
- mCallback.onAudioInfoChanged(info.getPlaybackType(),
- PlaybackInfo.getLegacyAudioStream(info), info.getVolumeControl(),
- info.getMaxVolume(), info.getCurrentVolume());
- }
- }
-}
diff --git a/media/api23/android/support/v4/media/MediaBrowserCompatApi23.java b/media/api23/android/support/v4/media/MediaBrowserCompatApi23.java
new file mode 100644
index 0000000..6f94d5d
--- /dev/null
+++ b/media/api23/android/support/v4/media/MediaBrowserCompatApi23.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 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.support.v4.media;
+
+import android.media.browse.MediaBrowser;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(23)
+class MediaBrowserCompatApi23 {
+
+ public static Object createItemCallback(ItemCallback callback) {
+ return new ItemCallbackProxy<>(callback);
+ }
+
+ public static void getItem(Object browserObj, String mediaId, Object itemCallbackObj) {
+ ((MediaBrowser) browserObj).getItem(mediaId, ((MediaBrowser.ItemCallback) itemCallbackObj));
+ }
+
+ interface ItemCallback {
+ void onItemLoaded(Parcel itemParcel);
+ void onError(@NonNull String itemId);
+ }
+
+ static class ItemCallbackProxy<T extends ItemCallback> extends MediaBrowser.ItemCallback {
+ protected final T mItemCallback;
+
+ public ItemCallbackProxy(T callback) {
+ mItemCallback = callback;
+ }
+
+ @Override
+ public void onItemLoaded(MediaBrowser.MediaItem item) {
+ if (item == null) {
+ mItemCallback.onItemLoaded(null);
+ } else {
+ Parcel parcel = Parcel.obtain();
+ item.writeToParcel(parcel, 0);
+ mItemCallback.onItemLoaded(parcel);
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String itemId) {
+ mItemCallback.onError(itemId);
+ }
+ }
+}
diff --git a/media/api23/android/support/v4/media/session/MediaControllerCompatApi23.java b/media/api23/android/support/v4/media/session/MediaControllerCompatApi23.java
new file mode 100644
index 0000000..ca424d1
--- /dev/null
+++ b/media/api23/android/support/v4/media/session/MediaControllerCompatApi23.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 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.support.v4.media.session;
+
+import android.media.session.MediaController;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(23)
+class MediaControllerCompatApi23 {
+
+ public static class TransportControls extends MediaControllerCompatApi21.TransportControls {
+ public static void playFromUri(Object controlsObj, Uri uri, Bundle extras) {
+ ((MediaController.TransportControls) controlsObj).playFromUri(uri, extras);
+ }
+ }
+}
diff --git a/media/api23/androidx/media/MediaBrowserCompatApi23.java b/media/api23/androidx/media/MediaBrowserCompatApi23.java
deleted file mode 100644
index ab05030..0000000
--- a/media/api23/androidx/media/MediaBrowserCompatApi23.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media;
-
-import android.media.browse.MediaBrowser;
-import android.os.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaBrowserCompatApi23 {
-
- public static Object createItemCallback(ItemCallback callback) {
- return new ItemCallbackProxy<>(callback);
- }
-
- public static void getItem(Object browserObj, String mediaId, Object itemCallbackObj) {
- ((MediaBrowser) browserObj).getItem(mediaId, ((MediaBrowser.ItemCallback) itemCallbackObj));
- }
-
- interface ItemCallback {
- void onItemLoaded(Parcel itemParcel);
- void onError(@NonNull String itemId);
- }
-
- static class ItemCallbackProxy<T extends ItemCallback> extends MediaBrowser.ItemCallback {
- protected final T mItemCallback;
-
- public ItemCallbackProxy(T callback) {
- mItemCallback = callback;
- }
-
- @Override
- public void onItemLoaded(MediaBrowser.MediaItem item) {
- if (item == null) {
- mItemCallback.onItemLoaded(null);
- } else {
- Parcel parcel = Parcel.obtain();
- item.writeToParcel(parcel, 0);
- mItemCallback.onItemLoaded(parcel);
- }
- }
-
- @Override
- public void onError(@NonNull String itemId) {
- mItemCallback.onError(itemId);
- }
- }
-}
diff --git a/media/api23/androidx/media/session/MediaControllerCompatApi23.java b/media/api23/androidx/media/session/MediaControllerCompatApi23.java
deleted file mode 100644
index 036f128..0000000
--- a/media/api23/androidx/media/session/MediaControllerCompatApi23.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media.session;
-
-import android.media.session.MediaController;
-import android.net.Uri;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(23)
-class MediaControllerCompatApi23 {
-
- public static class TransportControls extends MediaControllerCompatApi21.TransportControls {
- public static void playFromUri(Object controlsObj, Uri uri, Bundle extras) {
- ((MediaController.TransportControls) controlsObj).playFromUri(uri, extras);
- }
- }
-}
diff --git a/media/api24/android/support/v4/media/session/MediaControllerCompatApi24.java b/media/api24/android/support/v4/media/session/MediaControllerCompatApi24.java
new file mode 100644
index 0000000..9faeff6
--- /dev/null
+++ b/media/api24/android/support/v4/media/session/MediaControllerCompatApi24.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 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.support.v4.media.session;
+
+import android.media.session.MediaController;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(24)
+class MediaControllerCompatApi24 {
+
+ public static class TransportControls extends MediaControllerCompatApi23.TransportControls {
+ public static void prepare(Object controlsObj) {
+ ((MediaController.TransportControls) controlsObj).prepare();
+ }
+
+ public static void prepareFromMediaId(Object controlsObj, String mediaId, Bundle extras) {
+ ((MediaController.TransportControls) controlsObj).prepareFromMediaId(mediaId, extras);
+ }
+
+ public static void prepareFromSearch(Object controlsObj, String query, Bundle extras) {
+ ((MediaController.TransportControls) controlsObj).prepareFromSearch(query, extras);
+ }
+
+ public static void prepareFromUri(Object controlsObj, Uri uri, Bundle extras) {
+ ((MediaController.TransportControls) controlsObj).prepareFromUri(uri, extras);
+ }
+ }
+}
diff --git a/media/api24/androidx/media/session/MediaControllerCompatApi24.java b/media/api24/androidx/media/session/MediaControllerCompatApi24.java
deleted file mode 100644
index df5d74b..0000000
--- a/media/api24/androidx/media/session/MediaControllerCompatApi24.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media.session;
-
-import android.media.session.MediaController;
-import android.net.Uri;
-import android.os.Bundle;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(24)
-class MediaControllerCompatApi24 {
-
- public static class TransportControls extends MediaControllerCompatApi23.TransportControls {
- public static void prepare(Object controlsObj) {
- ((MediaController.TransportControls) controlsObj).prepare();
- }
-
- public static void prepareFromMediaId(Object controlsObj, String mediaId, Bundle extras) {
- ((MediaController.TransportControls) controlsObj).prepareFromMediaId(mediaId, extras);
- }
-
- public static void prepareFromSearch(Object controlsObj, String query, Bundle extras) {
- ((MediaController.TransportControls) controlsObj).prepareFromSearch(query, extras);
- }
-
- public static void prepareFromUri(Object controlsObj, Uri uri, Bundle extras) {
- ((MediaController.TransportControls) controlsObj).prepareFromUri(uri, extras);
- }
- }
-}
diff --git a/media/api26/android/support/v4/media/MediaBrowserCompatApi26.java b/media/api26/android/support/v4/media/MediaBrowserCompatApi26.java
new file mode 100644
index 0000000..5d556d3
--- /dev/null
+++ b/media/api26/android/support/v4/media/MediaBrowserCompatApi26.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.support.v4.media;
+
+import android.media.browse.MediaBrowser;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.List;
+
+@RequiresApi(26)
+class MediaBrowserCompatApi26 {
+ static Object createSubscriptionCallback(SubscriptionCallback callback) {
+ return new SubscriptionCallbackProxy<>(callback);
+ }
+
+ public static void subscribe(Object browserObj, String parentId, Bundle options,
+ Object subscriptionCallbackObj) {
+ ((MediaBrowser) browserObj).subscribe(parentId, options,
+ (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
+ }
+
+ public static void unsubscribe(Object browserObj, String parentId,
+ Object subscriptionCallbackObj) {
+ ((MediaBrowser) browserObj).unsubscribe(parentId,
+ (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
+ }
+
+ interface SubscriptionCallback extends MediaBrowserCompatApi21.SubscriptionCallback {
+ void onChildrenLoaded(@NonNull String parentId, List<?> children, @NonNull Bundle options);
+ void onError(@NonNull String parentId, @NonNull Bundle options);
+ }
+
+ static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
+ extends MediaBrowserCompatApi21.SubscriptionCallbackProxy<T> {
+ SubscriptionCallbackProxy(T callback) {
+ super(callback);
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId,
+ List<MediaBrowser.MediaItem> children, @NonNull Bundle options) {
+ mSubscriptionCallback.onChildrenLoaded(parentId, children, options);
+ }
+
+ @Override
+ public void onError(@NonNull String parentId, @NonNull Bundle options) {
+ mSubscriptionCallback.onError(parentId, options);
+ }
+ }
+}
diff --git a/media/api26/androidx/media/MediaBrowserCompatApi26.java b/media/api26/androidx/media/MediaBrowserCompatApi26.java
deleted file mode 100644
index 7311866..0000000
--- a/media/api26/androidx/media/MediaBrowserCompatApi26.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media;
-
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import java.util.List;
-
-@RequiresApi(26)
-class MediaBrowserCompatApi26 {
- static Object createSubscriptionCallback(SubscriptionCallback callback) {
- return new SubscriptionCallbackProxy<>(callback);
- }
-
- public static void subscribe(Object browserObj, String parentId, Bundle options,
- Object subscriptionCallbackObj) {
- ((MediaBrowser) browserObj).subscribe(parentId, options,
- (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
- }
-
- public static void unsubscribe(Object browserObj, String parentId,
- Object subscriptionCallbackObj) {
- ((MediaBrowser) browserObj).unsubscribe(parentId,
- (MediaBrowser.SubscriptionCallback) subscriptionCallbackObj);
- }
-
- interface SubscriptionCallback extends MediaBrowserCompatApi21.SubscriptionCallback {
- void onChildrenLoaded(@NonNull String parentId, List<?> children, @NonNull Bundle options);
- void onError(@NonNull String parentId, @NonNull Bundle options);
- }
-
- static class SubscriptionCallbackProxy<T extends SubscriptionCallback>
- extends MediaBrowserCompatApi21.SubscriptionCallbackProxy<T> {
- SubscriptionCallbackProxy(T callback) {
- super(callback);
- }
-
- @Override
- public void onChildrenLoaded(@NonNull String parentId,
- List<MediaBrowser.MediaItem> children, @NonNull Bundle options) {
- mSubscriptionCallback.onChildrenLoaded(parentId, children, options);
- }
-
- @Override
- public void onError(@NonNull String parentId, @NonNull Bundle options) {
- mSubscriptionCallback.onError(parentId, options);
- }
- }
-}
diff --git a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
new file mode 100644
index 0000000..a0e839b
--- /dev/null
+++ b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
@@ -0,0 +1,2289 @@
+/*
+ * Copyright 2018 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.support.v4.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
+import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
+import static androidx.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
+import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
+import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
+import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
+import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
+import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
+import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
+import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
+import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
+import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
+import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
+import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
+import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
+import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
+import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
+import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
+import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_2;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.BadParcelableException;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.support.v4.media.session.IMediaSession;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+import androidx.core.app.BundleCompat;
+import androidx.media.MediaBrowserCompatUtils;
+import androidx.media.MediaBrowserServiceCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Browses media content offered by a {@link MediaBrowserServiceCompat}.
+ * <p>
+ * This object is not thread-safe. All calls should happen on the thread on which the browser
+ * was constructed.
+ * </p><p>
+ * All callback methods will be called from the thread on which the browser was constructed.
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about building your media application, read the
+ * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
+ * </div>
+ */
+public final class MediaBrowserCompat {
+ static final String TAG = "MediaBrowserCompat";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ /**
+ * Used as an int extra field to denote the page number to subscribe.
+ * The value of {@code EXTRA_PAGE} should be greater than or equal to 1.
+ *
+ * @see android.service.media.MediaBrowserService.BrowserRoot
+ * @see #EXTRA_PAGE_SIZE
+ */
+ public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE";
+
+ /**
+ * Used as an int extra field to denote the number of media items in a page.
+ * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1.
+ *
+ * @see android.service.media.MediaBrowserService.BrowserRoot
+ * @see #EXTRA_PAGE
+ */
+ public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
+
+ /**
+ * Used as a string extra field to denote the target {@link MediaItem}.
+ *
+ * @see #CUSTOM_ACTION_DOWNLOAD
+ * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
+ */
+ public static final String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID";
+
+ /**
+ * Used as a float extra field to denote the current progress during download. The value of this
+ * field must be a float number within [0.0, 1.0].
+ *
+ * @see #CUSTOM_ACTION_DOWNLOAD
+ * @see CustomActionCallback#onProgressUpdate
+ */
+ public static final String EXTRA_DOWNLOAD_PROGRESS =
+ "android.media.browse.extra.DOWNLOAD_PROGRESS";
+
+ /**
+ * Predefined custom action to ask the connected service to download a specific
+ * {@link MediaItem} for offline playback. The id of the media item must be passed in an extra
+ * bundle. The download progress might be delivered to the browser via
+ * {@link CustomActionCallback#onProgressUpdate}.
+ *
+ * @see #EXTRA_MEDIA_ID
+ * @see #EXTRA_DOWNLOAD_PROGRESS
+ * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
+ */
+ public static final String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD";
+
+ /**
+ * Predefined custom action to ask the connected service to remove the downloaded file of
+ * {@link MediaItem} by the {@link #CUSTOM_ACTION_DOWNLOAD download} action. The id of the
+ * media item must be passed in an extra bundle.
+ *
+ * @see #EXTRA_MEDIA_ID
+ * @see #CUSTOM_ACTION_DOWNLOAD
+ */
+ public static final String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE =
+ "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE";
+
+ private final MediaBrowserImpl mImpl;
+
+ /**
+ * Creates a media browser for the specified media browse service.
+ *
+ * @param context The context.
+ * @param serviceComponent The component name of the media browse service.
+ * @param callback The connection callback.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browse service when connecting and retrieving the root id
+ * for browsing, or null if none. The contents of this bundle may affect
+ * the information returned when browsing.
+ * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
+ * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
+ * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
+ */
+ public MediaBrowserCompat(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
+ // and 25 devices, use the support library version of implementation on those devices.
+ if (Build.VERSION.SDK_INT >= 26) {
+ mImpl = new MediaBrowserImplApi26(context, serviceComponent, callback, rootHints);
+ } else if (Build.VERSION.SDK_INT >= 23) {
+ mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints);
+ } else {
+ mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints);
+ }
+ }
+
+ /**
+ * Connects to the media browse service.
+ * <p>
+ * The connection callback specified in the constructor will be invoked
+ * when the connection completes or fails.
+ * </p>
+ */
+ public void connect() {
+ mImpl.connect();
+ }
+
+ /**
+ * Disconnects from the media browse service.
+ * After this, no more callbacks will be received.
+ */
+ public void disconnect() {
+ mImpl.disconnect();
+ }
+
+ /**
+ * Returns whether the browser is connected to the service.
+ */
+ public boolean isConnected() {
+ return mImpl.isConnected();
+ }
+
+ /**
+ * Gets the service component that the media browser is connected to.
+ */
+ public @NonNull
+ ComponentName getServiceComponent() {
+ return mImpl.getServiceComponent();
+ }
+
+ /**
+ * Gets the root id.
+ * <p>
+ * Note that the root id may become invalid or change when when the
+ * browser is disconnected.
+ * </p>
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @NonNull String getRoot() {
+ return mImpl.getRoot();
+ }
+
+ /**
+ * Gets any extras for the media service.
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @Nullable
+ Bundle getExtras() {
+ return mImpl.getExtras();
+ }
+
+ /**
+ * Gets the media session token associated with the media browser.
+ * <p>
+ * Note that the session token may become invalid or change when when the
+ * browser is disconnected.
+ * </p>
+ *
+ * @return The session token for the browser, never null.
+ *
+ * @throws IllegalStateException if not connected.
+ */
+ public @NonNull MediaSessionCompat.Token getSessionToken() {
+ return mImpl.getSessionToken();
+ }
+
+ /**
+ * Queries for information about the media items that are contained within
+ * the specified id and subscribes to receive updates when they change.
+ * <p>
+ * The list of subscriptions is maintained even when not connected and is
+ * restored after the reconnection. It is ok to subscribe while not connected
+ * but the results will not be returned until the connection completes.
+ * </p>
+ * <p>
+ * If the id is already subscribed with a different callback then the new
+ * callback will replace the previous one and the child data will be
+ * reloaded.
+ * </p>
+ *
+ * @param parentId The id of the parent media item whose list of children
+ * will be subscribed.
+ * @param callback The callback to receive the list of children.
+ */
+ public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
+ // Check arguments.
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("parentId is empty");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+ mImpl.subscribe(parentId, null, callback);
+ }
+
+ /**
+ * Queries with service-specific arguments for information about the media items
+ * that are contained within the specified id and subscribes to receive updates
+ * when they change.
+ * <p>
+ * The list of subscriptions is maintained even when not connected and is
+ * restored after the reconnection. It is ok to subscribe while not connected
+ * but the results will not be returned until the connection completes.
+ * </p>
+ * <p>
+ * If the id is already subscribed with a different callback then the new
+ * callback will replace the previous one and the child data will be
+ * reloaded.
+ * </p>
+ *
+ * @param parentId The id of the parent media item whose list of children
+ * will be subscribed.
+ * @param options A bundle of service-specific arguments to send to the media
+ * browse service. The contents of this bundle may affect the
+ * information returned when browsing.
+ * @param callback The callback to receive the list of children.
+ */
+ public void subscribe(@NonNull String parentId, @NonNull Bundle options,
+ @NonNull SubscriptionCallback callback) {
+ // Check arguments.
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("parentId is empty");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+ if (options == null) {
+ throw new IllegalArgumentException("options are null");
+ }
+ mImpl.subscribe(parentId, options, callback);
+ }
+
+ /**
+ * Unsubscribes for changes to the children of the specified media id.
+ * <p>
+ * The query callback will no longer be invoked for results associated with
+ * this id once this method returns.
+ * </p>
+ *
+ * @param parentId The id of the parent media item whose list of children
+ * will be unsubscribed.
+ */
+ public void unsubscribe(@NonNull String parentId) {
+ // Check arguments.
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("parentId is empty");
+ }
+ mImpl.unsubscribe(parentId, null);
+ }
+
+ /**
+ * Unsubscribes for changes to the children of the specified media id.
+ * <p>
+ * The query callback will no longer be invoked for results associated with
+ * this id once this method returns.
+ * </p>
+ *
+ * @param parentId The id of the parent media item whose list of children
+ * will be unsubscribed.
+ * @param callback A callback sent to the media browse service to subscribe.
+ */
+ public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
+ // Check arguments.
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("parentId is empty");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+ mImpl.unsubscribe(parentId, callback);
+ }
+
+ /**
+ * Retrieves a specific {@link MediaItem} from the connected service. Not
+ * all services may support this, so falling back to subscribing to the
+ * parent's id should be used when unavailable.
+ *
+ * @param mediaId The id of the item to retrieve.
+ * @param cb The callback to receive the result on.
+ */
+ public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
+ mImpl.getItem(mediaId, cb);
+ }
+
+ /**
+ * Searches {@link MediaItem media items} from the connected service. Not all services may
+ * support this, and {@link SearchCallback#onError} will be called if not implemented.
+ *
+ * @param query The search query that contains keywords separated by space. Should not be an
+ * empty string.
+ * @param extras The bundle of service-specific arguments to send to the media browser service.
+ * The contents of this bundle may affect the search result.
+ * @param callback The callback to receive the search result. Must be non-null.
+ * @throws IllegalStateException if the browser is not connected to the media browser service.
+ */
+ public void search(@NonNull final String query, final Bundle extras,
+ @NonNull SearchCallback callback) {
+ if (TextUtils.isEmpty(query)) {
+ throw new IllegalArgumentException("query cannot be empty");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+ mImpl.search(query, extras, callback);
+ }
+
+ /**
+ * Sends a custom action to the connected service. If the service doesn't support the given
+ * action, {@link CustomActionCallback#onError} will be called.
+ *
+ * @param action The custom action that will be sent to the connected service. Should not be an
+ * empty string.
+ * @param extras The bundle of service-specific arguments to send to the media browser service.
+ * @param callback The callback to receive the result of the custom action.
+ * @see #CUSTOM_ACTION_DOWNLOAD
+ * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
+ */
+ public void sendCustomAction(@NonNull String action, Bundle extras,
+ @Nullable CustomActionCallback callback) {
+ if (TextUtils.isEmpty(action)) {
+ throw new IllegalArgumentException("action cannot be empty");
+ }
+ mImpl.sendCustomAction(action, extras, callback);
+ }
+
+ /**
+ * A class with information on a single media item for use in browsing/searching media.
+ * MediaItems are application dependent so we cannot guarantee that they contain the
+ * right values.
+ */
+ public static class MediaItem implements Parcelable {
+ private final int mFlags;
+ private final MediaDescriptionCompat mDescription;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+ public @interface Flags { }
+
+ /**
+ * Flag: Indicates that the item has children of its own.
+ */
+ public static final int FLAG_BROWSABLE = 1 << 0;
+
+ /**
+ * Flag: Indicates that the item is playable.
+ * <p>
+ * The id of this item may be passed to
+ * {@link TransportControls#playFromMediaId(String, Bundle)}
+ * to start playing it.
+ * </p>
+ */
+ public static final int FLAG_PLAYABLE = 1 << 1;
+
+ /**
+ * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem}
+ * object.
+ * <p>
+ * This method is only supported on API 21+. On API 20 and below, it returns null.
+ * </p>
+ *
+ * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object.
+ * @return An equivalent {@link MediaItem} object, or null if none.
+ */
+ public static MediaItem fromMediaItem(Object itemObj) {
+ if (itemObj == null || Build.VERSION.SDK_INT < 21) {
+ return null;
+ }
+ int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj);
+ MediaDescriptionCompat description =
+ MediaDescriptionCompat.fromMediaDescription(
+ MediaBrowserCompatApi21.MediaItem.getDescription(itemObj));
+ return new MediaItem(description, flags);
+ }
+
+ /**
+ * Creates a list of {@link MediaItem} objects from a framework
+ * {@link android.media.browse.MediaBrowser.MediaItem} object list.
+ * <p>
+ * This method is only supported on API 21+. On API 20 and below, it returns null.
+ * </p>
+ *
+ * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects.
+ * @return An equivalent list of {@link MediaItem} objects, or null if none.
+ */
+ public static List<MediaItem> fromMediaItemList(List<?> itemList) {
+ if (itemList == null || Build.VERSION.SDK_INT < 21) {
+ return null;
+ }
+ List<MediaItem> items = new ArrayList<>(itemList.size());
+ for (Object itemObj : itemList) {
+ items.add(fromMediaItem(itemObj));
+ }
+ return items;
+ }
+
+ /**
+ * Create a new MediaItem for use in browsing media.
+ * @param description The description of the media, which must include a
+ * media id.
+ * @param flags The flags for this item.
+ */
+ public MediaItem(@NonNull MediaDescriptionCompat description, @Flags int flags) {
+ if (description == null) {
+ throw new IllegalArgumentException("description cannot be null");
+ }
+ if (TextUtils.isEmpty(description.getMediaId())) {
+ throw new IllegalArgumentException("description must have a non-empty media id");
+ }
+ mFlags = flags;
+ mDescription = description;
+ }
+
+ /**
+ * Private constructor.
+ */
+ MediaItem(Parcel in) {
+ mFlags = in.readInt();
+ mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mFlags);
+ mDescription.writeToParcel(out, flags);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("MediaItem{");
+ sb.append("mFlags=").append(mFlags);
+ sb.append(", mDescription=").append(mDescription);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public static final Parcelable.Creator<MediaItem> CREATOR =
+ new Parcelable.Creator<MediaItem>() {
+ @Override
+ public MediaItem createFromParcel(Parcel in) {
+ return new MediaItem(in);
+ }
+
+ @Override
+ public MediaItem[] newArray(int size) {
+ return new MediaItem[size];
+ }
+ };
+
+ /**
+ * Gets the flags of the item.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether this item is browsable.
+ * @see #FLAG_BROWSABLE
+ */
+ public boolean isBrowsable() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ /**
+ * Returns whether this item is playable.
+ * @see #FLAG_PLAYABLE
+ */
+ public boolean isPlayable() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ /**
+ * Returns the description of the media.
+ */
+ public @NonNull MediaDescriptionCompat getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns the media id in the {@link MediaDescriptionCompat} for this item.
+ * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
+ */
+ public @Nullable String getMediaId() {
+ return mDescription.getMediaId();
+ }
+ }
+
+ /**
+ * Callbacks for connection related events.
+ */
+ public static class ConnectionCallback {
+ final Object mConnectionCallbackObj;
+ ConnectionCallbackInternal mConnectionCallbackInternal;
+
+ public ConnectionCallback() {
+ if (Build.VERSION.SDK_INT >= 21) {
+ mConnectionCallbackObj =
+ MediaBrowserCompatApi21.createConnectionCallback(new StubApi21());
+ } else {
+ mConnectionCallbackObj = null;
+ }
+ }
+
+ /**
+ * Invoked after {@link MediaBrowserCompat#connect()} when the request has successfully
+ * completed.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Invoked when the client is disconnected from the media browser.
+ */
+ public void onConnectionSuspended() {
+ }
+
+ /**
+ * Invoked when the connection to the media browser failed.
+ */
+ public void onConnectionFailed() {
+ }
+
+ void setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal) {
+ mConnectionCallbackInternal = connectionCallbackInternal;
+ }
+
+ interface ConnectionCallbackInternal {
+ void onConnected();
+ void onConnectionSuspended();
+ void onConnectionFailed();
+ }
+
+ private class StubApi21 implements MediaBrowserCompatApi21.ConnectionCallback {
+ StubApi21() {
+ }
+
+ @Override
+ public void onConnected() {
+ if (mConnectionCallbackInternal != null) {
+ mConnectionCallbackInternal.onConnected();
+ }
+ ConnectionCallback.this.onConnected();
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ if (mConnectionCallbackInternal != null) {
+ mConnectionCallbackInternal.onConnectionSuspended();
+ }
+ ConnectionCallback.this.onConnectionSuspended();
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ if (mConnectionCallbackInternal != null) {
+ mConnectionCallbackInternal.onConnectionFailed();
+ }
+ ConnectionCallback.this.onConnectionFailed();
+ }
+ }
+ }
+
+ /**
+ * Callbacks for subscription related events.
+ */
+ public static abstract class SubscriptionCallback {
+ private final Object mSubscriptionCallbackObj;
+ private final IBinder mToken;
+ WeakReference<Subscription> mSubscriptionRef;
+
+ public SubscriptionCallback() {
+ mToken = new Binder();
+ if (Build.VERSION.SDK_INT >= 26) {
+ mSubscriptionCallbackObj =
+ MediaBrowserCompatApi26.createSubscriptionCallback(new StubApi26());
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ mSubscriptionCallbackObj =
+ MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
+ } else {
+ mSubscriptionCallbackObj = null;
+ }
+ }
+
+ /**
+ * Called when the list of children is loaded or updated.
+ *
+ * @param parentId The media id of the parent media item.
+ * @param children The children which were loaded.
+ */
+ public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
+ }
+
+ /**
+ * Called when the list of children is loaded or updated.
+ *
+ * @param parentId The media id of the parent media item.
+ * @param children The children which were loaded.
+ * @param options A bundle of service-specific arguments to send to the media
+ * browse service. The contents of this bundle may affect the
+ * information returned when browsing.
+ */
+ public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
+ @NonNull Bundle options) {
+ }
+
+ /**
+ * Called when the id doesn't exist or other errors in subscribing.
+ * <p>
+ * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe}
+ * called, because some errors may heal themselves.
+ * </p>
+ *
+ * @param parentId The media id of the parent media item whose children could not be loaded.
+ */
+ public void onError(@NonNull String parentId) {
+ }
+
+ /**
+ * Called when the id doesn't exist or other errors in subscribing.
+ * <p>
+ * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe}
+ * called, because some errors may heal themselves.
+ * </p>
+ *
+ * @param parentId The media id of the parent media item whose children could
+ * not be loaded.
+ * @param options A bundle of service-specific arguments sent to the media
+ * browse service.
+ */
+ public void onError(@NonNull String parentId, @NonNull Bundle options) {
+ }
+
+ private void setSubscription(Subscription subscription) {
+ mSubscriptionRef = new WeakReference<>(subscription);
+ }
+
+ private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback {
+ StubApi21() {
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId, List<?> children) {
+ Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get();
+ if (sub == null) {
+ SubscriptionCallback.this.onChildrenLoaded(
+ parentId, MediaItem.fromMediaItemList(children));
+ } else {
+ List<MediaBrowserCompat.MediaItem> itemList =
+ MediaItem.fromMediaItemList(children);
+ final List<SubscriptionCallback> callbacks = sub.getCallbacks();
+ final List<Bundle> optionsList = sub.getOptionsList();
+ for (int i = 0; i < callbacks.size(); ++i) {
+ Bundle options = optionsList.get(i);
+ if (options == null) {
+ SubscriptionCallback.this.onChildrenLoaded(parentId, itemList);
+ } else {
+ SubscriptionCallback.this.onChildrenLoaded(
+ parentId, applyOptions(itemList, options), options);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String parentId) {
+ SubscriptionCallback.this.onError(parentId);
+ }
+
+ List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
+ final Bundle options) {
+ if (list == null) {
+ return null;
+ }
+ int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
+ int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
+ if (page == -1 && pageSize == -1) {
+ return list;
+ }
+ int fromIndex = pageSize * page;
+ int toIndex = fromIndex + pageSize;
+ if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
+ return Collections.EMPTY_LIST;
+ }
+ if (toIndex > list.size()) {
+ toIndex = list.size();
+ }
+ return list.subList(fromIndex, toIndex);
+ }
+
+ }
+
+ private class StubApi26 extends StubApi21
+ implements MediaBrowserCompatApi26.SubscriptionCallback {
+ StubApi26() {
+ }
+
+ @Override
+ public void onChildrenLoaded(@NonNull String parentId, List<?> children,
+ @NonNull Bundle options) {
+ SubscriptionCallback.this.onChildrenLoaded(
+ parentId, MediaItem.fromMediaItemList(children), options);
+ }
+
+ @Override
+ public void onError(@NonNull String parentId, @NonNull Bundle options) {
+ SubscriptionCallback.this.onError(parentId, options);
+ }
+ }
+ }
+
+ /**
+ * Callback for receiving the result of {@link #getItem}.
+ */
+ public static abstract class ItemCallback {
+ final Object mItemCallbackObj;
+
+ public ItemCallback() {
+ if (Build.VERSION.SDK_INT >= 23) {
+ mItemCallbackObj = MediaBrowserCompatApi23.createItemCallback(new StubApi23());
+ } else {
+ mItemCallbackObj = null;
+ }
+ }
+
+ /**
+ * Called when the item has been returned by the browser service.
+ *
+ * @param item The item that was returned or null if it doesn't exist.
+ */
+ public void onItemLoaded(MediaItem item) {
+ }
+
+ /**
+ * Called when the item doesn't exist or there was an error retrieving it.
+ *
+ * @param itemId The media id of the media item which could not be loaded.
+ */
+ public void onError(@NonNull String itemId) {
+ }
+
+ private class StubApi23 implements MediaBrowserCompatApi23.ItemCallback {
+ StubApi23() {
+ }
+
+ @Override
+ public void onItemLoaded(Parcel itemParcel) {
+ if (itemParcel == null) {
+ ItemCallback.this.onItemLoaded(null);
+ } else {
+ itemParcel.setDataPosition(0);
+ MediaItem item =
+ MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
+ itemParcel.recycle();
+ ItemCallback.this.onItemLoaded(item);
+ }
+ }
+
+ @Override
+ public void onError(@NonNull String itemId) {
+ ItemCallback.this.onError(itemId);
+ }
+ }
+ }
+
+ /**
+ * Callback for receiving the result of {@link #search}.
+ */
+ public abstract static class SearchCallback {
+ /**
+ * Called when the {@link #search} finished successfully.
+ *
+ * @param query The search query sent for the search request to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ * @param items The list of media items which contains the search result.
+ */
+ public void onSearchResult(@NonNull String query, Bundle extras,
+ @NonNull List<MediaItem> items) {
+ }
+
+ /**
+ * Called when an error happens while {@link #search} or the connected service doesn't
+ * support {@link #search}.
+ *
+ * @param query The search query sent for the search request to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ */
+ public void onError(@NonNull String query, Bundle extras) {
+ }
+ }
+
+ /**
+ * Callback for receiving the result of {@link #sendCustomAction}.
+ */
+ public abstract static class CustomActionCallback {
+ /**
+ * Called when an interim update was delivered from the connected service while performing
+ * the custom action.
+ *
+ * @param action The custom action sent to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ * @param data The additional data delivered from the connected service.
+ */
+ public void onProgressUpdate(String action, Bundle extras, Bundle data) {
+ }
+
+ /**
+ * Called when the custom action finished successfully.
+ *
+ * @param action The custom action sent to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ * @param resultData The additional data delivered from the connected service.
+ */
+ public void onResult(String action, Bundle extras, Bundle resultData) {
+ }
+
+ /**
+ * Called when an error happens while performing the custom action or the connected service
+ * doesn't support the requested custom action.
+ *
+ * @param action The custom action sent to the connected service.
+ * @param extras The bundle of service-specific arguments sent to the connected service.
+ * @param data The additional data delivered from the connected service.
+ */
+ public void onError(String action, Bundle extras, Bundle data) {
+ }
+ }
+
+ interface MediaBrowserImpl {
+ void connect();
+ void disconnect();
+ boolean isConnected();
+ ComponentName getServiceComponent();
+ @NonNull String getRoot();
+ @Nullable Bundle getExtras();
+ @NonNull MediaSessionCompat.Token getSessionToken();
+ void subscribe(@NonNull String parentId, @Nullable Bundle options,
+ @NonNull SubscriptionCallback callback);
+ void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
+ void getItem(@NonNull String mediaId, @NonNull ItemCallback cb);
+ void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
+ void sendCustomAction(@NonNull String action, Bundle extras,
+ @Nullable CustomActionCallback callback);
+ }
+
+ interface MediaBrowserServiceCallbackImpl {
+ void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session,
+ Bundle extra);
+ void onConnectionFailed(Messenger callback);
+ void onLoadChildren(Messenger callback, String parentId, List list, Bundle options);
+ }
+
+ static class MediaBrowserImplBase
+ implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl {
+ static final int CONNECT_STATE_DISCONNECTING = 0;
+ static final int CONNECT_STATE_DISCONNECTED = 1;
+ static final int CONNECT_STATE_CONNECTING = 2;
+ static final int CONNECT_STATE_CONNECTED = 3;
+ static final int CONNECT_STATE_SUSPENDED = 4;
+
+ final Context mContext;
+ final ComponentName mServiceComponent;
+ final ConnectionCallback mCallback;
+ final Bundle mRootHints;
+ final CallbackHandler mHandler = new CallbackHandler(this);
+ private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
+
+ int mState = CONNECT_STATE_DISCONNECTED;
+ MediaServiceConnection mServiceConnection;
+ ServiceBinderWrapper mServiceBinderWrapper;
+ Messenger mCallbacksMessenger;
+ private String mRootId;
+ private MediaSessionCompat.Token mMediaSessionToken;
+ private Bundle mExtras;
+
+ public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (serviceComponent == null) {
+ throw new IllegalArgumentException("service component must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("connection callback must not be null");
+ }
+ mContext = context;
+ mServiceComponent = serviceComponent;
+ mCallback = callback;
+ mRootHints = rootHints == null ? null : new Bundle(rootHints);
+ }
+
+ @Override
+ public void connect() {
+ if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
+ throw new IllegalStateException("connect() called while neigther disconnecting nor "
+ + "disconnected (state=" + getStateLabel(mState) + ")");
+ }
+
+ mState = CONNECT_STATE_CONNECTING;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // mState could be changed by the Runnable of disconnect()
+ if (mState == CONNECT_STATE_DISCONNECTING) {
+ return;
+ }
+ mState = CONNECT_STATE_CONNECTING;
+ // TODO: remove this extra check.
+ if (DEBUG) {
+ if (mServiceConnection != null) {
+ throw new RuntimeException("mServiceConnection should be null. Instead "
+ + "it is " + mServiceConnection);
+ }
+ }
+ if (mServiceBinderWrapper != null) {
+ throw new RuntimeException("mServiceBinderWrapper should be null. Instead "
+ + "it is " + mServiceBinderWrapper);
+ }
+ if (mCallbacksMessenger != null) {
+ throw new RuntimeException("mCallbacksMessenger should be null. Instead "
+ + "it is " + mCallbacksMessenger);
+ }
+
+ final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
+ intent.setComponent(mServiceComponent);
+
+ mServiceConnection = new MediaServiceConnection();
+ boolean bound = false;
+ try {
+ bound = mContext.bindService(intent, mServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed binding to service " + mServiceComponent);
+ }
+
+ if (!bound) {
+ // Tell them that it didn't work.
+ forceCloseConnection();
+ mCallback.onConnectionFailed();
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "connect...");
+ dump();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void disconnect() {
+ // It's ok to call this any state, because allowing this lets apps not have
+ // to check isConnected() unnecessarily. They won't appreciate the extra
+ // assertions for this. We do everything we can here to go back to a sane state.
+ mState = CONNECT_STATE_DISCONNECTING;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // connect() could be called before this. Then we will disconnect and reconnect.
+ if (mCallbacksMessenger != null) {
+ try {
+ mServiceBinderWrapper.disconnect(mCallbacksMessenger);
+ } catch (RemoteException ex) {
+ // We are disconnecting anyway. Log, just for posterity but it's not
+ // a big problem.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ }
+ }
+ int state = mState;
+ forceCloseConnection();
+ // If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that
+ // the operation came after disconnect() can be handled properly.
+ if (state != CONNECT_STATE_DISCONNECTING) {
+ mState = state;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "disconnect...");
+ dump();
+ }
+ }
+ });
+ }
+
+ /**
+ * Null out the variables and unbind from the service. This doesn't include
+ * calling disconnect on the service, because we only try to do that in the
+ * clean shutdown cases.
+ * <p>
+ * Everywhere that calls this EXCEPT for disconnect() should follow it with
+ * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
+ * for a clean shutdown, but everywhere else is a dirty shutdown and should
+ * notify the app.
+ */
+ void forceCloseConnection() {
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ mState = CONNECT_STATE_DISCONNECTED;
+ mServiceConnection = null;
+ mServiceBinderWrapper = null;
+ mCallbacksMessenger = null;
+ mHandler.setCallbacksMessenger(null);
+ mRootId = null;
+ mMediaSessionToken = null;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return mState == CONNECT_STATE_CONNECTED;
+ }
+
+ @Override
+ public @NonNull ComponentName getServiceComponent() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getServiceComponent() called while not connected" +
+ " (state=" + mState + ")");
+ }
+ return mServiceComponent;
+ }
+
+ @Override
+ public @NonNull String getRoot() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getRoot() called while not connected"
+ + "(state=" + getStateLabel(mState) + ")");
+ }
+ return mRootId;
+ }
+
+ @Override
+ public @Nullable Bundle getExtras() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getExtras() called while not connected (state="
+ + getStateLabel(mState) + ")");
+ }
+ return mExtras;
+ }
+
+ @Override
+ public @NonNull MediaSessionCompat.Token getSessionToken() {
+ if (!isConnected()) {
+ throw new IllegalStateException("getSessionToken() called while not connected"
+ + "(state=" + mState + ")");
+ }
+ return mMediaSessionToken;
+ }
+
+ @Override
+ public void subscribe(@NonNull String parentId, Bundle options,
+ @NonNull SubscriptionCallback callback) {
+ // Update or create the subscription.
+ Subscription sub = mSubscriptions.get(parentId);
+ if (sub == null) {
+ sub = new Subscription();
+ mSubscriptions.put(parentId, sub);
+ }
+ Bundle copiedOptions = options == null ? null : new Bundle(options);
+ sub.putCallback(mContext, copiedOptions, callback);
+
+ // If we are connected, tell the service that we are watching. If we aren't
+ // connected, the service will be told when we connect.
+ if (isConnected()) {
+ try {
+ mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
+ mCallbacksMessenger);
+ } catch (RemoteException e) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
+ }
+ }
+ }
+
+ @Override
+ public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
+ Subscription sub = mSubscriptions.get(parentId);
+ if (sub == null) {
+ return;
+ }
+
+ // Tell the service if necessary.
+ try {
+ if (callback == null) {
+ if (isConnected()) {
+ mServiceBinderWrapper.removeSubscription(parentId, null,
+ mCallbacksMessenger);
+ }
+ } else {
+ final List<SubscriptionCallback> callbacks = sub.getCallbacks();
+ final List<Bundle> optionsList = sub.getOptionsList();
+ for (int i = callbacks.size() - 1; i >= 0; --i) {
+ if (callbacks.get(i) == callback) {
+ if (isConnected()) {
+ mServiceBinderWrapper.removeSubscription(
+ parentId, callback.mToken, mCallbacksMessenger);
+ }
+ callbacks.remove(i);
+ optionsList.remove(i);
+ }
+ }
+ }
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
+ }
+
+ if (sub.isEmpty() || callback == null) {
+ mSubscriptions.remove(parentId);
+ }
+ }
+
+ @Override
+ public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
+ if (TextUtils.isEmpty(mediaId)) {
+ throw new IllegalArgumentException("mediaId is empty");
+ }
+ if (cb == null) {
+ throw new IllegalArgumentException("cb is null");
+ }
+ if (!isConnected()) {
+ Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError(mediaId);
+ }
+ });
+ return;
+ }
+ ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
+ try {
+ mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error getting media item: " + mediaId);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError(mediaId);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void search(@NonNull final String query, final Bundle extras,
+ @NonNull final SearchCallback callback) {
+ if (!isConnected()) {
+ throw new IllegalStateException("search() called while not connected"
+ + " (state=" + getStateLabel(mState) + ")");
+ }
+
+ ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+ try {
+ mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error searching items with query: " + query, e);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(query, extras);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void sendCustomAction(@NonNull final String action, final Bundle extras,
+ @Nullable final CustomActionCallback callback) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
+ + "extras " + extras + " because the browser is not connected to the "
+ + "service.");
+ }
+
+ ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
+ mHandler);
+ try {
+ mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
+ mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
+ + extras, e);
+ if (callback != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(action, extras, null);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onServiceConnected(final Messenger callback, final String root,
+ final MediaSessionCompat.Token session, final Bundle extra) {
+ // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
+ if (!isCurrent(callback, "onConnect")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState)
+ + "... ignoring");
+ return;
+ }
+ mRootId = root;
+ mMediaSessionToken = session;
+ mExtras = extra;
+ mState = CONNECT_STATE_CONNECTED;
+
+ if (DEBUG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mCallback.onConnected();
+
+ // we may receive some subscriptions before we are connected, so re-subscribe
+ // everything now
+ try {
+ for (Map.Entry<String, Subscription> subscriptionEntry
+ : mSubscriptions.entrySet()) {
+ String id = subscriptionEntry.getKey();
+ Subscription sub = subscriptionEntry.getValue();
+ List<SubscriptionCallback> callbackList = sub.getCallbacks();
+ List<Bundle> optionsList = sub.getOptionsList();
+ for (int i = 0; i < callbackList.size(); ++i) {
+ mServiceBinderWrapper.addSubscription(id, callbackList.get(i).mToken,
+ optionsList.get(i), mCallbacksMessenger);
+ }
+ }
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "addSubscription failed with RemoteException.");
+ }
+ }
+
+ @Override
+ public void onConnectionFailed(final Messenger callback) {
+ Log.e(TAG, "onConnectFailed for " + mServiceComponent);
+
+ // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
+ if (!isCurrent(callback, "onConnectFailed")) {
+ return;
+ }
+ // Don't allow them to call us twice.
+ if (mState != CONNECT_STATE_CONNECTING) {
+ Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState)
+ + "... ignoring");
+ return;
+ }
+
+ // Clean up
+ forceCloseConnection();
+
+ // Tell the app.
+ mCallback.onConnectionFailed();
+ }
+
+ @Override
+ public void onLoadChildren(final Messenger callback, final String parentId,
+ final List list, final Bundle options) {
+ // Check that there hasn't been a disconnect or a different ServiceConnection.
+ if (!isCurrent(callback, "onLoadChildren")) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
+ }
+
+ // Check that the subscription is still subscribed.
+ final Subscription subscription = mSubscriptions.get(parentId);
+ if (subscription == null) {
+ if (DEBUG) {
+ Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
+ }
+ return;
+ }
+
+ // Tell the app.
+ SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
+ if (subscriptionCallback != null) {
+ if (options == null) {
+ if (list == null) {
+ subscriptionCallback.onError(parentId);
+ } else {
+ subscriptionCallback.onChildrenLoaded(parentId, list);
+ }
+ } else {
+ if (list == null) {
+ subscriptionCallback.onError(parentId, options);
+ } else {
+ subscriptionCallback.onChildrenLoaded(parentId, list, options);
+ }
+ }
+ }
+ }
+
+ /**
+ * For debugging.
+ */
+ private static String getStateLabel(int state) {
+ switch (state) {
+ case CONNECT_STATE_DISCONNECTING:
+ return "CONNECT_STATE_DISCONNECTING";
+ case CONNECT_STATE_DISCONNECTED:
+ return "CONNECT_STATE_DISCONNECTED";
+ case CONNECT_STATE_CONNECTING:
+ return "CONNECT_STATE_CONNECTING";
+ case CONNECT_STATE_CONNECTED:
+ return "CONNECT_STATE_CONNECTED";
+ case CONNECT_STATE_SUSPENDED:
+ return "CONNECT_STATE_SUSPENDED";
+ default:
+ return "UNKNOWN/" + state;
+ }
+ }
+
+ /**
+ * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private boolean isCurrent(Messenger callback, String funcName) {
+ if (mCallbacksMessenger != callback || mState == CONNECT_STATE_DISCONNECTING
+ || mState == CONNECT_STATE_DISCONNECTED) {
+ if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
+ Log.i(TAG, funcName + " for " + mServiceComponent + " with mCallbacksMessenger="
+ + mCallbacksMessenger + " this=" + this);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Log internal state.
+ */
+ void dump() {
+ Log.d(TAG, "MediaBrowserCompat...");
+ Log.d(TAG, " mServiceComponent=" + mServiceComponent);
+ Log.d(TAG, " mCallback=" + mCallback);
+ Log.d(TAG, " mRootHints=" + mRootHints);
+ Log.d(TAG, " mState=" + getStateLabel(mState));
+ Log.d(TAG, " mServiceConnection=" + mServiceConnection);
+ Log.d(TAG, " mServiceBinderWrapper=" + mServiceBinderWrapper);
+ Log.d(TAG, " mCallbacksMessenger=" + mCallbacksMessenger);
+ Log.d(TAG, " mRootId=" + mRootId);
+ Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
+ }
+
+ /**
+ * ServiceConnection to the other app.
+ */
+ private class MediaServiceConnection implements ServiceConnection {
+ MediaServiceConnection() {
+ }
+
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder binder) {
+ postOrRun(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
+ + " binder=" + binder);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't
+ // called disconnect().
+ if (!isCurrent("onServiceConnected")) {
+ return;
+ }
+
+ // Save their binder
+ mServiceBinderWrapper = new ServiceBinderWrapper(binder, mRootHints);
+
+ // We make a new mServiceCallbacks each time we connect so that we can drop
+ // responses from previous connections.
+ mCallbacksMessenger = new Messenger(mHandler);
+ mHandler.setCallbacksMessenger(mCallbacksMessenger);
+
+ mState = CONNECT_STATE_CONNECTING;
+
+ // Call connect, which is async. When we get a response from that we will
+ // say that we're connected.
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ mServiceBinderWrapper.connect(mContext, mCallbacksMessenger);
+ } catch (RemoteException ex) {
+ // Connect failed, which isn't good. But the auto-reconnect on the
+ // service will take over and we will come back. We will also get the
+ // onServiceDisconnected, which has all the cleanup code. So let that
+ // do it.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ if (DEBUG) {
+ Log.d(TAG, "ServiceCallbacks.onConnect...");
+ dump();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ postOrRun(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
+ + " this=" + this + " mServiceConnection=" +
+ mServiceConnection);
+ dump();
+ }
+
+ // Make sure we are still the current connection, and that they haven't
+ // called disconnect().
+ if (!isCurrent("onServiceDisconnected")) {
+ return;
+ }
+
+ // Clear out what we set in onServiceConnected
+ mServiceBinderWrapper = null;
+ mCallbacksMessenger = null;
+ mHandler.setCallbacksMessenger(null);
+
+ // And tell the app that it's suspended.
+ mState = CONNECT_STATE_SUSPENDED;
+ mCallback.onConnectionSuspended();
+ }
+ });
+ }
+
+ private void postOrRun(Runnable r) {
+ if (Thread.currentThread() == mHandler.getLooper().getThread()) {
+ r.run();
+ } else {
+ mHandler.post(r);
+ }
+ }
+
+ /**
+ * Return true if this is the current ServiceConnection. Also logs if it's not.
+ */
+ boolean isCurrent(String funcName) {
+ if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING
+ || mState == CONNECT_STATE_DISCONNECTED) {
+ if (mState != CONNECT_STATE_DISCONNECTING
+ && mState != CONNECT_STATE_DISCONNECTED) {
+ // Check mState, because otherwise this log is noisy.
+ Log.i(TAG, funcName + " for " + mServiceComponent +
+ " with mServiceConnection=" + mServiceConnection + " this=" + this);
+ }
+ return false;
+ }
+ return true;
+ }
+ }
+ }
+
+ @RequiresApi(21)
+ static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl,
+ ConnectionCallback.ConnectionCallbackInternal {
+ final Context mContext;
+ protected final Object mBrowserObj;
+ protected final Bundle mRootHints;
+ protected final CallbackHandler mHandler = new CallbackHandler(this);
+ private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
+
+ protected int mServiceVersion;
+ protected ServiceBinderWrapper mServiceBinderWrapper;
+ protected Messenger mCallbacksMessenger;
+ private MediaSessionCompat.Token mMediaSessionToken;
+
+ MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ mContext = context;
+ if (rootHints == null) {
+ rootHints = new Bundle();
+ }
+ rootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT);
+ mRootHints = new Bundle(rootHints);
+ callback.setInternalConnectionCallback(this);
+ mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent,
+ callback.mConnectionCallbackObj, mRootHints);
+ }
+
+ @Override
+ public void connect() {
+ MediaBrowserCompatApi21.connect(mBrowserObj);
+ }
+
+ @Override
+ public void disconnect() {
+ if (mServiceBinderWrapper != null && mCallbacksMessenger != null) {
+ try {
+ mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error unregistering client messenger." );
+ }
+ }
+ MediaBrowserCompatApi21.disconnect(mBrowserObj);
+ }
+
+ @Override
+ public boolean isConnected() {
+ return MediaBrowserCompatApi21.isConnected(mBrowserObj);
+ }
+
+ @Override
+ public ComponentName getServiceComponent() {
+ return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj);
+ }
+
+ @NonNull
+ @Override
+ public String getRoot() {
+ return MediaBrowserCompatApi21.getRoot(mBrowserObj);
+ }
+
+ @Nullable
+ @Override
+ public Bundle getExtras() {
+ return MediaBrowserCompatApi21.getExtras(mBrowserObj);
+ }
+
+ @NonNull
+ @Override
+ public MediaSessionCompat.Token getSessionToken() {
+ if (mMediaSessionToken == null) {
+ mMediaSessionToken = MediaSessionCompat.Token.fromToken(
+ MediaBrowserCompatApi21.getSessionToken(mBrowserObj));
+ }
+ return mMediaSessionToken;
+ }
+
+ @Override
+ public void subscribe(@NonNull final String parentId, final Bundle options,
+ @NonNull final SubscriptionCallback callback) {
+ // Update or create the subscription.
+ Subscription sub = mSubscriptions.get(parentId);
+ if (sub == null) {
+ sub = new Subscription();
+ mSubscriptions.put(parentId, sub);
+ }
+ callback.setSubscription(sub);
+ Bundle copiedOptions = options == null ? null : new Bundle(options);
+ sub.putCallback(mContext, copiedOptions, callback);
+
+ if (mServiceBinderWrapper == null) {
+ // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
+ // subscribe with options won't work properly.
+ MediaBrowserCompatApi21.subscribe(
+ mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
+ } else {
+ try {
+ mServiceBinderWrapper.addSubscription(
+ parentId, callback.mToken, copiedOptions, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.i(TAG, "Remote error subscribing media item: " + parentId);
+ }
+ }
+ }
+
+ @Override
+ public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
+ Subscription sub = mSubscriptions.get(parentId);
+ if (sub == null) {
+ return;
+ }
+
+ if (mServiceBinderWrapper == null) {
+ if (callback == null) {
+ MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+ } else {
+ final List<SubscriptionCallback> callbacks = sub.getCallbacks();
+ final List<Bundle> optionsList = sub.getOptionsList();
+ for (int i = callbacks.size() - 1; i >= 0; --i) {
+ if (callbacks.get(i) == callback) {
+ callbacks.remove(i);
+ optionsList.remove(i);
+ }
+ }
+ if (callbacks.size() == 0) {
+ MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+ }
+ }
+ } else {
+ // Tell the service if necessary.
+ try {
+ if (callback == null) {
+ mServiceBinderWrapper.removeSubscription(parentId, null,
+ mCallbacksMessenger);
+ } else {
+ final List<SubscriptionCallback> callbacks = sub.getCallbacks();
+ final List<Bundle> optionsList = sub.getOptionsList();
+ for (int i = callbacks.size() - 1; i >= 0; --i) {
+ if (callbacks.get(i) == callback) {
+ mServiceBinderWrapper.removeSubscription(
+ parentId, callback.mToken, mCallbacksMessenger);
+ callbacks.remove(i);
+ optionsList.remove(i);
+ }
+ }
+ }
+ } catch (RemoteException ex) {
+ // Process is crashing. We will disconnect, and upon reconnect we will
+ // automatically reregister. So nothing to do here.
+ Log.d(TAG, "removeSubscription failed with RemoteException parentId="
+ + parentId);
+ }
+ }
+
+ if (sub.isEmpty() || callback == null) {
+ mSubscriptions.remove(parentId);
+ }
+ }
+
+ @Override
+ public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
+ if (TextUtils.isEmpty(mediaId)) {
+ throw new IllegalArgumentException("mediaId is empty");
+ }
+ if (cb == null) {
+ throw new IllegalArgumentException("cb is null");
+ }
+ if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) {
+ Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError(mediaId);
+ }
+ });
+ return;
+ }
+ if (mServiceBinderWrapper == null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Default framework implementation.
+ cb.onError(mediaId);
+ }
+ });
+ return;
+ }
+ ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
+ try {
+ mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error getting media item: " + mediaId);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ cb.onError(mediaId);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void search(@NonNull final String query, final Bundle extras,
+ @NonNull final SearchCallback callback) {
+ if (!isConnected()) {
+ throw new IllegalStateException("search() called while not connected");
+ }
+ if (mServiceBinderWrapper == null) {
+ Log.i(TAG, "The connected service doesn't support search.");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Default framework implementation.
+ callback.onError(query, extras);
+ }
+ });
+ return;
+ }
+
+ ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+ try {
+ mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error searching items with query: " + query, e);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(query, extras);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void sendCustomAction(@NonNull final String action, final Bundle extras,
+ @Nullable final CustomActionCallback callback) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
+ + "extras " + extras + " because the browser is not connected to the "
+ + "service.");
+ }
+ if (mServiceBinderWrapper == null) {
+ Log.i(TAG, "The connected service doesn't support sendCustomAction.");
+ if (callback != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(action, extras, null);
+ }
+ });
+ }
+ }
+
+ ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
+ mHandler);
+ try {
+ mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
+ mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
+ + extras, e);
+ if (callback != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(action, extras, null);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onConnected() {
+ Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
+ if (extras == null) {
+ return;
+ }
+ mServiceVersion = extras.getInt(EXTRA_SERVICE_VERSION, 0);
+ IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER);
+ if (serviceBinder != null) {
+ mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints);
+ mCallbacksMessenger = new Messenger(mHandler);
+ mHandler.setCallbacksMessenger(mCallbacksMessenger);
+ try {
+ mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger);
+ } catch (RemoteException e) {
+ Log.i(TAG, "Remote error registering client messenger." );
+ }
+ }
+ IMediaSession sessionToken = IMediaSession.Stub.asInterface(
+ BundleCompat.getBinder(extras, EXTRA_SESSION_BINDER));
+ if (sessionToken != null) {
+ mMediaSessionToken = MediaSessionCompat.Token.fromToken(
+ MediaBrowserCompatApi21.getSessionToken(mBrowserObj), sessionToken);
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ mServiceBinderWrapper = null;
+ mCallbacksMessenger = null;
+ mMediaSessionToken = null;
+ mHandler.setCallbacksMessenger(null);
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ // Do noting
+ }
+
+ @Override
+ public void onServiceConnected(final Messenger callback, final String root,
+ final MediaSessionCompat.Token session, final Bundle extra) {
+ // This method will not be called.
+ }
+
+ @Override
+ public void onConnectionFailed(Messenger callback) {
+ // This method will not be called.
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) {
+ if (mCallbacksMessenger != callback) {
+ return;
+ }
+
+ // Check that the subscription is still subscribed.
+ Subscription subscription = mSubscriptions.get(parentId);
+ if (subscription == null) {
+ if (DEBUG) {
+ Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
+ }
+ return;
+ }
+
+ // Tell the app.
+ SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
+ if (subscriptionCallback != null) {
+ if (options == null) {
+ if (list == null) {
+ subscriptionCallback.onError(parentId);
+ } else {
+ subscriptionCallback.onChildrenLoaded(parentId, list);
+ }
+ } else {
+ if (list == null) {
+ subscriptionCallback.onError(parentId, options);
+ } else {
+ subscriptionCallback.onChildrenLoaded(parentId, list, options);
+ }
+ }
+ }
+ }
+ }
+
+ @RequiresApi(23)
+ static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 {
+ MediaBrowserImplApi23(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ super(context, serviceComponent, callback, rootHints);
+ }
+
+ @Override
+ public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
+ if (mServiceBinderWrapper == null) {
+ MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj);
+ } else {
+ super.getItem(mediaId, cb);
+ }
+ }
+ }
+
+ @RequiresApi(26)
+ static class MediaBrowserImplApi26 extends MediaBrowserImplApi23 {
+ MediaBrowserImplApi26(Context context, ComponentName serviceComponent,
+ ConnectionCallback callback, Bundle rootHints) {
+ super(context, serviceComponent, callback, rootHints);
+ }
+
+ @Override
+ public void subscribe(@NonNull String parentId, @Nullable Bundle options,
+ @NonNull SubscriptionCallback callback) {
+ // From service v2, we use compat code when subscribing.
+ // This is to prevent ClassNotFoundException when options has Parcelable in it.
+ if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
+ if (options == null) {
+ MediaBrowserCompatApi21.subscribe(
+ mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
+ } else {
+ MediaBrowserCompatApi26.subscribe(
+ mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj);
+ }
+ } else {
+ super.subscribe(parentId, options, callback);
+ }
+ }
+
+ @Override
+ public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
+ // From service v2, we use compat code when subscribing.
+ // This is to prevent ClassNotFoundException when options has Parcelable in it.
+ if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
+ if (callback == null) {
+ MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
+ } else {
+ MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId,
+ callback.mSubscriptionCallbackObj);
+ }
+ } else {
+ super.unsubscribe(parentId, callback);
+ }
+ }
+ }
+
+ private static class Subscription {
+ private final List<SubscriptionCallback> mCallbacks;
+ private final List<Bundle> mOptionsList;
+
+ public Subscription() {
+ mCallbacks = new ArrayList<>();
+ mOptionsList = new ArrayList<>();
+ }
+
+ public boolean isEmpty() {
+ return mCallbacks.isEmpty();
+ }
+
+ public List<Bundle> getOptionsList() {
+ return mOptionsList;
+ }
+
+ public List<SubscriptionCallback> getCallbacks() {
+ return mCallbacks;
+ }
+
+ public SubscriptionCallback getCallback(Context context, Bundle options) {
+ if (options != null) {
+ options.setClassLoader(context.getClassLoader());
+ }
+ for (int i = 0; i < mOptionsList.size(); ++i) {
+ if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
+ return mCallbacks.get(i);
+ }
+ }
+ return null;
+ }
+
+ public void putCallback(Context context, Bundle options, SubscriptionCallback callback) {
+ if (options != null) {
+ options.setClassLoader(context.getClassLoader());
+ }
+ for (int i = 0; i < mOptionsList.size(); ++i) {
+ if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
+ mCallbacks.set(i, callback);
+ return;
+ }
+ }
+ mCallbacks.add(callback);
+ mOptionsList.add(options);
+ }
+ }
+
+ private static class CallbackHandler extends Handler {
+ private final WeakReference<MediaBrowserServiceCallbackImpl> mCallbackImplRef;
+ private WeakReference<Messenger> mCallbacksMessengerRef;
+
+ CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) {
+ super();
+ mCallbackImplRef = new WeakReference<>(callbackImpl);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mCallbacksMessengerRef == null || mCallbacksMessengerRef.get() == null ||
+ mCallbackImplRef.get() == null) {
+ return;
+ }
+ Bundle data = msg.getData();
+ data.setClassLoader(MediaSessionCompat.class.getClassLoader());
+ MediaBrowserServiceCallbackImpl serviceCallback = mCallbackImplRef.get();
+ Messenger callbacksMessenger = mCallbacksMessengerRef.get();
+ try {
+ switch (msg.what) {
+ case SERVICE_MSG_ON_CONNECT:
+ serviceCallback.onServiceConnected(callbacksMessenger,
+ data.getString(DATA_MEDIA_ITEM_ID),
+ (MediaSessionCompat.Token) data.getParcelable(
+ DATA_MEDIA_SESSION_TOKEN),
+ data.getBundle(DATA_ROOT_HINTS));
+ break;
+ case SERVICE_MSG_ON_CONNECT_FAILED:
+ serviceCallback.onConnectionFailed(callbacksMessenger);
+ break;
+ case SERVICE_MSG_ON_LOAD_CHILDREN:
+ serviceCallback.onLoadChildren(callbacksMessenger,
+ data.getString(DATA_MEDIA_ITEM_ID),
+ data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST),
+ data.getBundle(DATA_OPTIONS));
+ break;
+ default:
+ Log.w(TAG, "Unhandled message: " + msg
+ + "\n Client version: " + CLIENT_VERSION_CURRENT
+ + "\n Service version: " + msg.arg1);
+ }
+ } catch (BadParcelableException e) {
+ // Do not print the exception here, since it is already done by the Parcel class.
+ Log.e(TAG, "Could not unparcel the data.");
+ // If an error happened while connecting, disconnect from the service.
+ if (msg.what == SERVICE_MSG_ON_CONNECT) {
+ serviceCallback.onConnectionFailed(callbacksMessenger);
+ }
+ }
+ }
+
+ void setCallbacksMessenger(Messenger callbacksMessenger) {
+ mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger);
+ }
+ }
+
+ private static class ServiceBinderWrapper {
+ private Messenger mMessenger;
+ private Bundle mRootHints;
+
+ public ServiceBinderWrapper(IBinder target, Bundle rootHints) {
+ mMessenger = new Messenger(target);
+ mRootHints = rootHints;
+ }
+
+ void connect(Context context, Messenger callbacksMessenger)
+ throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_PACKAGE_NAME, context.getPackageName());
+ data.putBundle(DATA_ROOT_HINTS, mRootHints);
+ sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger);
+ }
+
+ void disconnect(Messenger callbacksMessenger) throws RemoteException {
+ sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger);
+ }
+
+ void addSubscription(String parentId, IBinder callbackToken, Bundle options,
+ Messenger callbacksMessenger)
+ throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_MEDIA_ITEM_ID, parentId);
+ BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
+ data.putBundle(DATA_OPTIONS, options);
+ sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger);
+ }
+
+ void removeSubscription(String parentId, IBinder callbackToken,
+ Messenger callbacksMessenger)
+ throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_MEDIA_ITEM_ID, parentId);
+ BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
+ sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger);
+ }
+
+ void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger)
+ throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_MEDIA_ITEM_ID, mediaId);
+ data.putParcelable(DATA_RESULT_RECEIVER, receiver);
+ sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger);
+ }
+
+ void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
+ Bundle data = new Bundle();
+ data.putBundle(DATA_ROOT_HINTS, mRootHints);
+ sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger);
+ }
+
+ void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
+ sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
+ }
+
+ void search(String query, Bundle extras, ResultReceiver receiver,
+ Messenger callbacksMessenger) throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_SEARCH_QUERY, query);
+ data.putBundle(DATA_SEARCH_EXTRAS, extras);
+ data.putParcelable(DATA_RESULT_RECEIVER, receiver);
+ sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
+ }
+
+ void sendCustomAction(String action, Bundle extras, ResultReceiver receiver,
+ Messenger callbacksMessenger) throws RemoteException {
+ Bundle data = new Bundle();
+ data.putString(DATA_CUSTOM_ACTION, action);
+ data.putBundle(DATA_CUSTOM_ACTION_EXTRAS, extras);
+ data.putParcelable(DATA_RESULT_RECEIVER, receiver);
+ sendRequest(CLIENT_MSG_SEND_CUSTOM_ACTION, data, callbacksMessenger);
+ }
+
+ private void sendRequest(int what, Bundle data, Messenger cbMessenger)
+ throws RemoteException {
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.arg1 = CLIENT_VERSION_CURRENT;
+ msg.setData(data);
+ msg.replyTo = cbMessenger;
+ mMessenger.send(msg);
+ }
+ }
+
+ private static class ItemReceiver extends ResultReceiver {
+ private final String mMediaId;
+ private final ItemCallback mCallback;
+
+ ItemReceiver(String mediaId, ItemCallback callback, Handler handler) {
+ super(handler);
+ mMediaId = mediaId;
+ mCallback = callback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultData != null) {
+ resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
+ }
+ if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
+ || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
+ mCallback.onError(mMediaId);
+ return;
+ }
+ Parcelable item = resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
+ if (item == null || item instanceof MediaItem) {
+ mCallback.onItemLoaded((MediaItem) item);
+ } else {
+ mCallback.onError(mMediaId);
+ }
+ }
+ }
+
+ private static class SearchResultReceiver extends ResultReceiver {
+ private final String mQuery;
+ private final Bundle mExtras;
+ private final SearchCallback mCallback;
+
+ SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
+ Handler handler) {
+ super(handler);
+ mQuery = query;
+ mExtras = extras;
+ mCallback = callback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultData != null) {
+ resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
+ }
+ if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
+ || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
+ mCallback.onError(mQuery, mExtras);
+ return;
+ }
+ Parcelable[] items = resultData.getParcelableArray(
+ MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
+ List<MediaItem> results = null;
+ if (items != null) {
+ results = new ArrayList<>();
+ for (Parcelable item : items) {
+ results.add((MediaItem) item);
+ }
+ }
+ mCallback.onSearchResult(mQuery, mExtras, results);
+ }
+ }
+
+ private static class CustomActionResultReceiver extends ResultReceiver {
+ private final String mAction;
+ private final Bundle mExtras;
+ private final CustomActionCallback mCallback;
+
+ CustomActionResultReceiver(String action, Bundle extras, CustomActionCallback callback,
+ Handler handler) {
+ super(handler);
+ mAction = action;
+ mExtras = extras;
+ mCallback = callback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (mCallback == null) {
+ return;
+ }
+ switch (resultCode) {
+ case MediaBrowserServiceCompat.RESULT_PROGRESS_UPDATE:
+ mCallback.onProgressUpdate(mAction, mExtras, resultData);
+ break;
+ case MediaBrowserServiceCompat.RESULT_OK:
+ mCallback.onResult(mAction, mExtras, resultData);
+ break;
+ case MediaBrowserServiceCompat.RESULT_ERROR:
+ mCallback.onError(mAction, mExtras, resultData);
+ break;
+ default:
+ Log.w(TAG, "Unknown result code: " + resultCode + " (extras=" + mExtras
+ + ", resultData=" + resultData + ")");
+ break;
+ }
+ }
+ }
+}
diff --git a/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java b/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
index 3403427..723cb3b 100644
--- a/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaDescriptionCompat.java
@@ -27,7 +27,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.media.MediaBrowserCompat;
/**
* A simple set of metadata for a media item suitable for display. This can be
diff --git a/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java b/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java
index be5e07c..435d78e 100644
--- a/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaMetadataCompat.java
@@ -23,14 +23,13 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
import androidx.collection.ArrayMap;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.session.MediaControllerCompat.TransportControls;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java b/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java
new file mode 100644
index 0000000..5e6f4ea
--- /dev/null
+++ b/media/src/main/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -0,0 +1,2489 @@
+/*
+ * Copyright 2018 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.support.v4.media.session;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.session.MediaController;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.app.BundleCompat;
+import androidx.core.app.SupportActivity;
+import androidx.media.VolumeProviderCompat;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Allows an app to interact with an ongoing media session. Media buttons and
+ * other commands can be sent to the session. A callback may be registered to
+ * receive updates from the session, such as metadata and play state changes.
+ * <p>
+ * A MediaController can be created if you have a {@link MediaSessionCompat.Token}
+ * from the session owner.
+ * <p>
+ * MediaController objects are thread-safe.
+ * <p>
+ * This is a helper for accessing features in {@link android.media.session.MediaSession}
+ * introduced after API level 4 in a backwards compatible fashion.
+ * <p class="note">
+ * If MediaControllerCompat is created with a {@link MediaSessionCompat.Token session token}
+ * from another process, following methods will not work directly after the creation if the
+ * {@link MediaSessionCompat.Token session token} is not passed through a
+ * {@link MediaBrowserCompat}:
+ * <ul>
+ * <li>{@link #getPlaybackState()}.{@link PlaybackStateCompat#getExtras() getExtras()}</li>
+ * <li>{@link #isCaptioningEnabled()}</li>
+ * <li>{@link #getRepeatMode()}</li>
+ * <li>{@link #getShuffleMode()}</li>
+ * </ul></p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about building your media application, read the
+ * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
+ * </div>
+ */
+public final class MediaControllerCompat {
+ static final String TAG = "MediaControllerCompat";
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_GET_EXTRA_BINDER =
+ "android.support.v4.media.session.command.GET_EXTRA_BINDER";
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_ADD_QUEUE_ITEM =
+ "android.support.v4.media.session.command.ADD_QUEUE_ITEM";
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_ADD_QUEUE_ITEM_AT =
+ "android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT";
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_REMOVE_QUEUE_ITEM =
+ "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM";
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_REMOVE_QUEUE_ITEM_AT =
+ "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT";
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION =
+ "android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION";
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final String COMMAND_ARGUMENT_INDEX =
+ "android.support.v4.media.session.command.ARGUMENT_INDEX";
+
+ private static class MediaControllerExtraData extends SupportActivity.ExtraData {
+ private final MediaControllerCompat mMediaController;
+
+ MediaControllerExtraData(MediaControllerCompat mediaController) {
+ mMediaController = mediaController;
+ }
+
+ MediaControllerCompat getMediaController() {
+ return mMediaController;
+ }
+ }
+
+ /**
+ * Sets a {@link MediaControllerCompat} in the {@code activity} for later retrieval via
+ * {@link #getMediaController(Activity)}.
+ *
+ * <p>This is compatible with {@link Activity#setMediaController(MediaController)}.
+ * If {@code activity} inherits {@link androidx.fragment.app.FragmentActivity}, the
+ * {@code mediaController} will be saved in the {@code activity}. In addition to that,
+ * on API 21 and later, {@link Activity#setMediaController(MediaController)} will be
+ * called.</p>
+ *
+ * @param activity The activity to set the {@code mediaController} in, must not be null.
+ * @param mediaController The controller for the session which should receive
+ * media keys and volume changes on API 21 and later.
+ * @see #getMediaController(Activity)
+ * @see Activity#setMediaController(android.media.session.MediaController)
+ */
+ public static void setMediaController(@NonNull Activity activity,
+ MediaControllerCompat mediaController) {
+ if (activity instanceof SupportActivity) {
+ ((SupportActivity) activity).putExtraData(
+ new MediaControllerExtraData(mediaController));
+ }
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ Object controllerObj = null;
+ if (mediaController != null) {
+ Object sessionTokenObj = mediaController.getSessionToken().getToken();
+ controllerObj = MediaControllerCompatApi21.fromToken(activity, sessionTokenObj);
+ }
+ MediaControllerCompatApi21.setMediaController(activity, controllerObj);
+ }
+ }
+
+ /**
+ * Retrieves the {@link MediaControllerCompat} set in the activity by
+ * {@link #setMediaController(Activity, MediaControllerCompat)} for sending media key and volume
+ * events.
+ *
+ * <p>This is compatible with {@link Activity#getMediaController()}.</p>
+ *
+ * @param activity The activity to get the media controller from, must not be null.
+ * @return The controller which should receive events.
+ * @see #setMediaController(Activity, MediaControllerCompat)
+ */
+ public static MediaControllerCompat getMediaController(@NonNull Activity activity) {
+ if (activity instanceof SupportActivity) {
+ MediaControllerExtraData extraData =
+ ((SupportActivity) activity).getExtraData(MediaControllerExtraData.class);
+ return extraData != null ? extraData.getMediaController() : null;
+ } else if (android.os.Build.VERSION.SDK_INT >= 21) {
+ Object controllerObj = MediaControllerCompatApi21.getMediaController(activity);
+ if (controllerObj == null) {
+ return null;
+ }
+ Object sessionTokenObj = MediaControllerCompatApi21.getSessionToken(controllerObj);
+ try {
+ return new MediaControllerCompat(activity,
+ MediaSessionCompat.Token.fromToken(sessionTokenObj));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getMediaController.", e);
+ }
+ }
+ return null;
+ }
+
+ private static void validateCustomAction(String action, Bundle args) {
+ if (action == null) {
+ return;
+ }
+ switch(action) {
+ case MediaSessionCompat.ACTION_FOLLOW:
+ case MediaSessionCompat.ACTION_UNFOLLOW:
+ if (args == null
+ || !args.containsKey(MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE)) {
+ throw new IllegalArgumentException("An extra field "
+ + MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE + " is required "
+ + "for this action " + action + ".");
+ }
+ break;
+ }
+ }
+
+ private final MediaControllerImpl mImpl;
+ private final MediaSessionCompat.Token mToken;
+ // This set is used to keep references to registered callbacks to prevent them being GCed,
+ // since we only keep weak references for callbacks in this class and its inner classes.
+ private final HashSet<Callback> mRegisteredCallbacks = new HashSet<>();
+
+ /**
+ * Creates a media controller from a session.
+ *
+ * @param session The session to be controlled.
+ */
+ public MediaControllerCompat(Context context, @NonNull MediaSessionCompat session) {
+ if (session == null) {
+ throw new IllegalArgumentException("session must not be null");
+ }
+ mToken = session.getSessionToken();
+
+ if (android.os.Build.VERSION.SDK_INT >= 24) {
+ mImpl = new MediaControllerImplApi24(context, session);
+ } else if (android.os.Build.VERSION.SDK_INT >= 23) {
+ mImpl = new MediaControllerImplApi23(context, session);
+ } else if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaControllerImplApi21(context, session);
+ } else {
+ mImpl = new MediaControllerImplBase(mToken);
+ }
+ }
+
+ /**
+ * Creates a media controller from a session token which may have
+ * been obtained from another process.
+ *
+ * @param sessionToken The token of the session to be controlled.
+ * @throws RemoteException if the session is not accessible.
+ */
+ public MediaControllerCompat(Context context, @NonNull MediaSessionCompat.Token sessionToken)
+ throws RemoteException {
+ if (sessionToken == null) {
+ throw new IllegalArgumentException("sessionToken must not be null");
+ }
+ mToken = sessionToken;
+
+ if (android.os.Build.VERSION.SDK_INT >= 24) {
+ mImpl = new MediaControllerImplApi24(context, sessionToken);
+ } else if (android.os.Build.VERSION.SDK_INT >= 23) {
+ mImpl = new MediaControllerImplApi23(context, sessionToken);
+ } else if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaControllerImplApi21(context, sessionToken);
+ } else {
+ mImpl = new MediaControllerImplBase(mToken);
+ }
+ }
+
+ /**
+ * Gets a {@link TransportControls} instance for this session.
+ *
+ * @return A controls instance
+ */
+ public TransportControls getTransportControls() {
+ return mImpl.getTransportControls();
+ }
+
+ /**
+ * Sends the specified media button event to the session. Only media keys can
+ * be sent by this method, other keys will be ignored.
+ *
+ * @param keyEvent The media button event to dispatch.
+ * @return true if the event was sent to the session, false otherwise.
+ */
+ public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) {
+ if (keyEvent == null) {
+ throw new IllegalArgumentException("KeyEvent may not be null");
+ }
+ return mImpl.dispatchMediaButtonEvent(keyEvent);
+ }
+
+ /**
+ * Gets the current playback state for this session.
+ *
+ * <p>If the session is not ready, {@link PlaybackStateCompat#getExtras()} on the result of
+ * this method may return null. </p>
+ *
+ * @return The current PlaybackState or null
+ * @see #isSessionReady
+ * @see Callback#onSessionReady
+ */
+ public PlaybackStateCompat getPlaybackState() {
+ return mImpl.getPlaybackState();
+ }
+
+ /**
+ * Gets the current metadata for this session.
+ *
+ * @return The current MediaMetadata or null.
+ */
+ public MediaMetadataCompat getMetadata() {
+ return mImpl.getMetadata();
+ }
+
+ /**
+ * Gets the current play queue for this session if one is set. If you only
+ * care about the current item {@link #getMetadata()} should be used.
+ *
+ * @return The current play queue or null.
+ */
+ public List<QueueItem> getQueue() {
+ return mImpl.getQueue();
+ }
+
+ /**
+ * Adds a queue item from the given {@code description} at the end of the play queue
+ * of this session. Not all sessions may support this. To know whether the session supports
+ * this, get the session's flags with {@link #getFlags()} and check that the flag
+ * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
+ *
+ * @param description The {@link MediaDescriptionCompat} for creating the
+ * {@link MediaSessionCompat.QueueItem} to be inserted.
+ * @throws UnsupportedOperationException If this session doesn't support this.
+ * @see #getFlags()
+ * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+ */
+ public void addQueueItem(MediaDescriptionCompat description) {
+ mImpl.addQueueItem(description);
+ }
+
+ /**
+ * Adds a queue item from the given {@code description} at the specified position
+ * in the play queue of this session. Shifts the queue item currently at that position
+ * (if any) and any subsequent queue items to the right (adds one to their indices).
+ * Not all sessions may support this. To know whether the session supports this,
+ * get the session's flags with {@link #getFlags()} and check that the flag
+ * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
+ *
+ * @param description The {@link MediaDescriptionCompat} for creating the
+ * {@link MediaSessionCompat.QueueItem} to be inserted.
+ * @param index The index at which the created {@link MediaSessionCompat.QueueItem}
+ * is to be inserted.
+ * @throws UnsupportedOperationException If this session doesn't support this.
+ * @see #getFlags()
+ * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+ */
+ public void addQueueItem(MediaDescriptionCompat description, int index) {
+ mImpl.addQueueItem(description, index);
+ }
+
+ /**
+ * Removes the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
+ * with the given {@link MediaDescriptionCompat description} in the play queue of the
+ * associated session. Not all sessions may support this. To know whether the session supports
+ * this, get the session's flags with {@link #getFlags()} and check that the flag
+ * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
+ *
+ * @param description The {@link MediaDescriptionCompat} for denoting the
+ * {@link MediaSessionCompat.QueueItem} to be removed.
+ * @throws UnsupportedOperationException If this session doesn't support this.
+ * @see #getFlags()
+ * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+ */
+ public void removeQueueItem(MediaDescriptionCompat description) {
+ mImpl.removeQueueItem(description);
+ }
+
+ /**
+ * Removes an queue item at the specified position in the play queue
+ * of this session. Not all sessions may support this. To know whether the session supports
+ * this, get the session's flags with {@link #getFlags()} and check that the flag
+ * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
+ *
+ * @param index The index of the element to be removed.
+ * @throws UnsupportedOperationException If this session doesn't support this.
+ * @see #getFlags()
+ * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
+ * @deprecated Use {@link #removeQueueItem(MediaDescriptionCompat)} instead.
+ */
+ @Deprecated
+ public void removeQueueItemAt(int index) {
+ List<QueueItem> queue = getQueue();
+ if (queue != null && index >= 0 && index < queue.size()) {
+ QueueItem item = queue.get(index);
+ if (item != null) {
+ removeQueueItem(item.getDescription());
+ }
+ }
+ }
+
+ /**
+ * Gets the queue title for this session.
+ */
+ public CharSequence getQueueTitle() {
+ return mImpl.getQueueTitle();
+ }
+
+ /**
+ * Gets the extras for this session.
+ */
+ public Bundle getExtras() {
+ return mImpl.getExtras();
+ }
+
+ /**
+ * Gets the rating type supported by the session. One of:
+ * <ul>
+ * <li>{@link RatingCompat#RATING_NONE}</li>
+ * <li>{@link RatingCompat#RATING_HEART}</li>
+ * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
+ * <li>{@link RatingCompat#RATING_3_STARS}</li>
+ * <li>{@link RatingCompat#RATING_4_STARS}</li>
+ * <li>{@link RatingCompat#RATING_5_STARS}</li>
+ * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
+ * </ul>
+ * <p>If the session is not ready, it will return {@link RatingCompat#RATING_NONE}.</p>
+ *
+ * @return The supported rating type, or {@link RatingCompat#RATING_NONE} if the value is not
+ * set or the session is not ready.
+ * @see #isSessionReady
+ * @see Callback#onSessionReady
+ */
+ public int getRatingType() {
+ return mImpl.getRatingType();
+ }
+
+ /**
+ * Returns whether captioning is enabled for this session.
+ *
+ * <p>If the session is not ready, it will return a {@code false}.</p>
+ *
+ * @return {@code true} if captioning is enabled, {@code false} if disabled or not set.
+ * @see #isSessionReady
+ * @see Callback#onSessionReady
+ */
+ public boolean isCaptioningEnabled() {
+ return mImpl.isCaptioningEnabled();
+ }
+
+ /**
+ * Gets the repeat mode for this session.
+ *
+ * @return The latest repeat mode set to the session,
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set, or
+ * {@link PlaybackStateCompat#REPEAT_MODE_INVALID} if the session is not ready yet.
+ * @see #isSessionReady
+ * @see Callback#onSessionReady
+ */
+ public int getRepeatMode() {
+ return mImpl.getRepeatMode();
+ }
+
+ /**
+ * Gets the shuffle mode for this session.
+ *
+ * @return The latest shuffle mode set to the session, or
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE} if disabled or not set, or
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_INVALID} if the session is not ready yet.
+ * @see #isSessionReady
+ * @see Callback#onSessionReady
+ */
+ public int getShuffleMode() {
+ return mImpl.getShuffleMode();
+ }
+
+ /**
+ * Gets the flags for this session. Flags are defined in
+ * {@link MediaSessionCompat}.
+ *
+ * @return The current set of flags for the session.
+ */
+ public long getFlags() {
+ return mImpl.getFlags();
+ }
+
+ /**
+ * Gets the current playback info for this session.
+ *
+ * @return The current playback info or null.
+ */
+ public PlaybackInfo getPlaybackInfo() {
+ return mImpl.getPlaybackInfo();
+ }
+
+ /**
+ * Gets an intent for launching UI associated with this session if one
+ * exists.
+ *
+ * @return A {@link PendingIntent} to launch UI or null.
+ */
+ public PendingIntent getSessionActivity() {
+ return mImpl.getSessionActivity();
+ }
+
+ /**
+ * Gets the token for the session this controller is connected to.
+ *
+ * @return The session's token.
+ */
+ public MediaSessionCompat.Token getSessionToken() {
+ return mToken;
+ }
+
+ /**
+ * Sets the volume of the output this session is playing on. The command will
+ * be ignored if it does not support
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
+ * {@link AudioManager} may be used to affect the handling.
+ *
+ * @see #getPlaybackInfo()
+ * @param value The value to set it to, between 0 and the reported max.
+ * @param flags Flags from {@link AudioManager} to include with the volume
+ * request.
+ */
+ public void setVolumeTo(int value, int flags) {
+ mImpl.setVolumeTo(value, flags);
+ }
+
+ /**
+ * Adjusts the volume of the output this session is playing on. The direction
+ * must be one of {@link AudioManager#ADJUST_LOWER},
+ * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+ * The command will be ignored if the session does not support
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
+ * {@link AudioManager} may be used to affect the handling.
+ *
+ * @see #getPlaybackInfo()
+ * @param direction The direction to adjust the volume in.
+ * @param flags Any flags to pass with the command.
+ */
+ public void adjustVolume(int direction, int flags) {
+ mImpl.adjustVolume(direction, flags);
+ }
+
+ /**
+ * Adds a callback to receive updates from the Session. Updates will be
+ * posted on the caller's thread.
+ *
+ * @param callback The callback object, must not be null.
+ */
+ public void registerCallback(@NonNull Callback callback) {
+ registerCallback(callback, null);
+ }
+
+ /**
+ * Adds a callback to receive updates from the session. Updates will be
+ * posted on the specified handler's thread.
+ *
+ * @param callback The callback object, must not be null.
+ * @param handler The handler to post updates on. If null the callers thread
+ * will be used.
+ */
+ public void registerCallback(@NonNull Callback callback, Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ if (handler == null) {
+ handler = new Handler();
+ }
+ callback.setHandler(handler);
+ mImpl.registerCallback(callback, handler);
+ mRegisteredCallbacks.add(callback);
+ }
+
+ /**
+ * Stops receiving updates on the specified callback. If an update has
+ * already been posted you may still receive it after calling this method.
+ *
+ * @param callback The callback to remove
+ */
+ public void unregisterCallback(@NonNull Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ try {
+ mRegisteredCallbacks.remove(callback);
+ mImpl.unregisterCallback(callback);
+ } finally {
+ callback.setHandler(null);
+ }
+ }
+
+ /**
+ * Sends a generic command to the session. It is up to the session creator
+ * to decide what commands and parameters they will support. As such,
+ * commands should only be sent to sessions that the controller owns.
+ *
+ * @param command The command to send
+ * @param params Any parameters to include with the command
+ * @param cb The callback to receive the result on
+ */
+ public void sendCommand(@NonNull String command, Bundle params, ResultReceiver cb) {
+ if (TextUtils.isEmpty(command)) {
+ throw new IllegalArgumentException("command must neither be null nor empty");
+ }
+ mImpl.sendCommand(command, params, cb);
+ }
+
+ /**
+ * Returns whether the session is ready or not.
+ *
+ * <p>If the session is not ready, following methods can work incorrectly.</p>
+ * <ul>
+ * <li>{@link #getPlaybackState()}</li>
+ * <li>{@link #getRatingType()}</li>
+ * <li>{@link #getRepeatMode()}</li>
+ * <li>{@link #getShuffleMode()}</li>
+ * <li>{@link #isCaptioningEnabled()}</li>
+ * </ul>
+ *
+ * @return true if the session is ready, false otherwise.
+ * @see Callback#onSessionReady()
+ */
+ public boolean isSessionReady() {
+ return mImpl.isSessionReady();
+ }
+
+ /**
+ * Gets the session owner's package name.
+ *
+ * @return The package name of of the session owner.
+ */
+ public String getPackageName() {
+ return mImpl.getPackageName();
+ }
+
+ /**
+ * Gets the underlying framework
+ * {@link android.media.session.MediaController} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return The underlying {@link android.media.session.MediaController}
+ * object, or null if none.
+ */
+ public Object getMediaController() {
+ return mImpl.getMediaController();
+ }
+
+ /**
+ * Callback for receiving updates on from the session. A Callback can be
+ * registered using {@link #registerCallback}
+ */
+ public static abstract class Callback implements IBinder.DeathRecipient {
+ private final Object mCallbackObj;
+ MessageHandler mHandler;
+ boolean mHasExtraCallback;
+
+ public Callback() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this));
+ } else {
+ mCallbackObj = new StubCompat(this);
+ }
+ }
+
+ /**
+ * Override to handle the session being ready.
+ *
+ * @see MediaControllerCompat#isSessionReady
+ */
+ public void onSessionReady() {
+ }
+
+ /**
+ * Override to handle the session being destroyed. The session is no
+ * longer valid after this call and calls to it will be ignored.
+ */
+ public void onSessionDestroyed() {
+ }
+
+ /**
+ * Override to handle custom events sent by the session owner without a
+ * specified interface. Controllers should only handle these for
+ * sessions they own.
+ *
+ * @param event The event from the session.
+ * @param extras Optional parameters for the event.
+ */
+ public void onSessionEvent(String event, Bundle extras) {
+ }
+
+ /**
+ * Override to handle changes in playback state.
+ *
+ * @param state The new playback state of the session
+ */
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ }
+
+ /**
+ * Override to handle changes to the current metadata.
+ *
+ * @param metadata The current metadata for the session or null if none.
+ * @see MediaMetadataCompat
+ */
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ }
+
+ /**
+ * Override to handle changes to items in the queue.
+ *
+ * @see MediaSessionCompat.QueueItem
+ * @param queue A list of items in the current play queue. It should
+ * include the currently playing item as well as previous and
+ * upcoming items if applicable.
+ */
+ public void onQueueChanged(List<QueueItem> queue) {
+ }
+
+ /**
+ * Override to handle changes to the queue title.
+ *
+ * @param title The title that should be displayed along with the play
+ * queue such as "Now Playing". May be null if there is no
+ * such title.
+ */
+ public void onQueueTitleChanged(CharSequence title) {
+ }
+
+ /**
+ * Override to handle changes to the {@link MediaSessionCompat} extras.
+ *
+ * @param extras The extras that can include other information
+ * associated with the {@link MediaSessionCompat}.
+ */
+ public void onExtrasChanged(Bundle extras) {
+ }
+
+ /**
+ * Override to handle changes to the audio info.
+ *
+ * @param info The current audio info for this session.
+ */
+ public void onAudioInfoChanged(PlaybackInfo info) {
+ }
+
+ /**
+ * Override to handle changes to the captioning enabled status.
+ *
+ * @param enabled {@code true} if captioning is enabled, {@code false} otherwise.
+ */
+ public void onCaptioningEnabledChanged(boolean enabled) {
+ }
+
+ /**
+ * Override to handle changes to the repeat mode.
+ *
+ * @param repeatMode The repeat mode. It should be one of followings:
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ALL},
+ * {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
+ */
+ public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ }
+
+ /**
+ * Override to handle changes to the shuffle mode.
+ *
+ * @param shuffleMode The shuffle mode. Must be one of the followings:
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
+ */
+ public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
+ }
+
+ @Override
+ public void binderDied() {
+ onSessionDestroyed();
+ }
+
+ /**
+ * Set the handler to use for callbacks.
+ */
+ void setHandler(Handler handler) {
+ if (handler == null) {
+ if (mHandler != null) {
+ mHandler.mRegistered = false;
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler = null;
+ }
+ } else {
+ mHandler = new MessageHandler(handler.getLooper());
+ mHandler.mRegistered = true;
+ }
+ }
+
+ void postToHandler(int what, Object obj, Bundle data) {
+ if (mHandler != null) {
+ Message msg = mHandler.obtainMessage(what, obj);
+ msg.setData(data);
+ msg.sendToTarget();
+ }
+ }
+
+ private static class StubApi21 implements MediaControllerCompatApi21.Callback {
+ private final WeakReference<MediaControllerCompat.Callback> mCallback;
+
+ StubApi21(MediaControllerCompat.Callback callback) {
+ mCallback = new WeakReference<>(callback);
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.onSessionDestroyed();
+ }
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ if (callback.mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) {
+ // Ignore. ExtraCallback will handle this.
+ } else {
+ callback.onSessionEvent(event, extras);
+ }
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(Object stateObj) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ if (callback.mHasExtraCallback) {
+ // Ignore. ExtraCallback will handle this.
+ } else {
+ callback.onPlaybackStateChanged(
+ PlaybackStateCompat.fromPlaybackState(stateObj));
+ }
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(Object metadataObj) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj));
+ }
+ }
+
+ @Override
+ public void onQueueChanged(List<?> queue) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.onQueueChanged(QueueItem.fromQueueItemList(queue));
+ }
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.onQueueTitleChanged(title);
+ }
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.onExtrasChanged(extras);
+ }
+ }
+
+ @Override
+ public void onAudioInfoChanged(
+ int type, int stream, int control, int max, int current) {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.onAudioInfoChanged(
+ new PlaybackInfo(type, stream, control, max, current));
+ }
+ }
+ }
+
+ private static class StubCompat extends IMediaControllerCallback.Stub {
+ private final WeakReference<MediaControllerCompat.Callback> mCallback;
+
+ StubCompat(MediaControllerCompat.Callback callback) {
+ mCallback = new WeakReference<>(callback);
+ }
+
+ @Override
+ public void onEvent(String event, Bundle extras) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_EVENT, event, extras);
+ }
+ }
+
+ @Override
+ public void onSessionDestroyed() throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_DESTROYED, null, null);
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
+ }
+ }
+
+ @Override
+ public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
+ }
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
+ }
+ }
+
+ @Override
+ public void onCaptioningEnabledChanged(boolean enabled) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(
+ MessageHandler.MSG_UPDATE_CAPTIONING_ENABLED, enabled, null);
+ }
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
+ }
+ }
+
+ @Override
+ public void onShuffleModeChangedRemoved(boolean enabled) throws RemoteException {
+ // Do nothing.
+ }
+
+ @Override
+ public void onShuffleModeChanged(int shuffleMode) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(
+ MessageHandler.MSG_UPDATE_SHUFFLE_MODE, shuffleMode, null);
+ }
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
+ }
+ }
+
+ @Override
+ public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ PlaybackInfo pi = null;
+ if (info != null) {
+ pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
+ info.maxVolume, info.currentVolume);
+ }
+ callback.postToHandler(MessageHandler.MSG_UPDATE_VOLUME, pi, null);
+ }
+ }
+
+ @Override
+ public void onSessionReady() throws RemoteException {
+ MediaControllerCompat.Callback callback = mCallback.get();
+ if (callback != null) {
+ callback.postToHandler(MessageHandler.MSG_SESSION_READY, null, null);
+ }
+ }
+ }
+
+ private class MessageHandler extends Handler {
+ private static final int MSG_EVENT = 1;
+ private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
+ private static final int MSG_UPDATE_METADATA = 3;
+ private static final int MSG_UPDATE_VOLUME = 4;
+ private static final int MSG_UPDATE_QUEUE = 5;
+ private static final int MSG_UPDATE_QUEUE_TITLE = 6;
+ private static final int MSG_UPDATE_EXTRAS = 7;
+ private static final int MSG_DESTROYED = 8;
+ private static final int MSG_UPDATE_REPEAT_MODE = 9;
+ private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11;
+ private static final int MSG_UPDATE_SHUFFLE_MODE = 12;
+ private static final int MSG_SESSION_READY = 13;
+
+ boolean mRegistered = false;
+
+ MessageHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (!mRegistered) {
+ return;
+ }
+ switch (msg.what) {
+ case MSG_EVENT:
+ onSessionEvent((String) msg.obj, msg.getData());
+ break;
+ case MSG_UPDATE_PLAYBACK_STATE:
+ onPlaybackStateChanged((PlaybackStateCompat) msg.obj);
+ break;
+ case MSG_UPDATE_METADATA:
+ onMetadataChanged((MediaMetadataCompat) msg.obj);
+ break;
+ case MSG_UPDATE_QUEUE:
+ onQueueChanged((List<QueueItem>) msg.obj);
+ break;
+ case MSG_UPDATE_QUEUE_TITLE:
+ onQueueTitleChanged((CharSequence) msg.obj);
+ break;
+ case MSG_UPDATE_CAPTIONING_ENABLED:
+ onCaptioningEnabledChanged((boolean) msg.obj);
+ break;
+ case MSG_UPDATE_REPEAT_MODE:
+ onRepeatModeChanged((int) msg.obj);
+ break;
+ case MSG_UPDATE_SHUFFLE_MODE:
+ onShuffleModeChanged((int) msg.obj);
+ break;
+ case MSG_UPDATE_EXTRAS:
+ onExtrasChanged((Bundle) msg.obj);
+ break;
+ case MSG_UPDATE_VOLUME:
+ onAudioInfoChanged((PlaybackInfo) msg.obj);
+ break;
+ case MSG_DESTROYED:
+ onSessionDestroyed();
+ break;
+ case MSG_SESSION_READY:
+ onSessionReady();
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Interface for controlling media playback on a session. This allows an app
+ * to send media transport commands to the session.
+ */
+ public static abstract class TransportControls {
+ /**
+ * Used as an integer extra field in {@link #playFromMediaId(String, Bundle)} or
+ * {@link #prepareFromMediaId(String, Bundle)} to indicate the stream type to be used by the
+ * media player when playing or preparing the specified media id. See {@link AudioManager}
+ * for a list of stream types.
+ */
+ public static final String EXTRA_LEGACY_STREAM_TYPE =
+ "android.media.session.extra.LEGACY_STREAM_TYPE";
+
+ TransportControls() {
+ }
+
+ /**
+ * Request that the player prepare for playback. This can decrease the time it takes to
+ * start playback when a play command is received. Preparation is not required. You can
+ * call {@link #play} without calling this method beforehand.
+ */
+ public abstract void prepare();
+
+ /**
+ * Request that the player prepare playback for a specific media id. This can decrease the
+ * time it takes to start playback when a play command is received. Preparation is not
+ * required. You can call {@link #playFromMediaId} without calling this method beforehand.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public abstract void prepareFromMediaId(String mediaId, Bundle extras);
+
+ /**
+ * Request that the player prepare playback for a specific search query. This can decrease
+ * the time it takes to start playback when a play command is received. An empty or null
+ * query should be treated as a request to prepare any music. Preparation is not required.
+ * You can call {@link #playFromSearch} without calling this method beforehand.
+ *
+ * @param query The search query.
+ * @param extras Optional extras that can include extra information
+ * about the query.
+ */
+ public abstract void prepareFromSearch(String query, Bundle extras);
+
+ /**
+ * Request that the player prepare playback for a specific {@link Uri}. This can decrease
+ * the time it takes to start playback when a play command is received. Preparation is not
+ * required. You can call {@link #playFromUri} without calling this method beforehand.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public abstract void prepareFromUri(Uri uri, Bundle extras);
+
+ /**
+ * Request that the player start its playback at its current position.
+ */
+ public abstract void play();
+
+ /**
+ * Request that the player start playback for a specific {@link Uri}.
+ *
+ * @param mediaId The uri of the requested media.
+ * @param extras Optional extras that can include extra information
+ * about the media item to be played.
+ */
+ public abstract void playFromMediaId(String mediaId, Bundle extras);
+
+ /**
+ * Request that the player start playback for a specific search query.
+ * An empty or null query should be treated as a request to play any
+ * music.
+ *
+ * @param query The search query.
+ * @param extras Optional extras that can include extra information
+ * about the query.
+ */
+ public abstract void playFromSearch(String query, Bundle extras);
+
+ /**
+ * Request that the player start playback for a specific {@link Uri}.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public abstract void playFromUri(Uri uri, Bundle extras);
+
+ /**
+ * Plays an item with a specific id in the play queue. If you specify an
+ * id that is not in the play queue, the behavior is undefined.
+ */
+ public abstract void skipToQueueItem(long id);
+
+ /**
+ * Request that the player pause its playback and stay at its current
+ * position.
+ */
+ public abstract void pause();
+
+ /**
+ * Request that the player stop its playback; it may clear its state in
+ * whatever way is appropriate.
+ */
+ public abstract void stop();
+
+ /**
+ * Moves to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public abstract void seekTo(long pos);
+
+ /**
+ * Starts fast forwarding. If playback is already fast forwarding this
+ * may increase the rate.
+ */
+ public abstract void fastForward();
+
+ /**
+ * Skips to the next item.
+ */
+ public abstract void skipToNext();
+
+ /**
+ * Starts rewinding. If playback is already rewinding this may increase
+ * the rate.
+ */
+ public abstract void rewind();
+
+ /**
+ * Skips to the previous item.
+ */
+ public abstract void skipToPrevious();
+
+ /**
+ * Rates the current content. This will cause the rating to be set for
+ * the current user. The rating type of the given {@link RatingCompat} must match the type
+ * returned by {@link #getRatingType()}.
+ *
+ * @param rating The rating to set for the current content
+ */
+ public abstract void setRating(RatingCompat rating);
+
+ /**
+ * Rates a media item. This will cause the rating to be set for
+ * the specific media item. The rating type of the given {@link RatingCompat} must match
+ * the type returned by {@link #getRatingType()}.
+ *
+ * @param rating The rating to set for the media item.
+ * @param extras Optional arguments that can include information about the media item
+ * to be rated.
+ *
+ * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE
+ * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE_VALUE
+ */
+ public abstract void setRating(RatingCompat rating, Bundle extras);
+
+ /**
+ * Enables/disables captioning for this session.
+ *
+ * @param enabled {@code true} to enable captioning, {@code false} to disable.
+ */
+ public abstract void setCaptioningEnabled(boolean enabled);
+
+ /**
+ * Sets the repeat mode for this session.
+ *
+ * @param repeatMode The repeat mode. Must be one of the followings:
+ * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
+ * {@link PlaybackStateCompat#REPEAT_MODE_ALL},
+ * {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
+ */
+ public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
+
+ /**
+ * Sets the shuffle mode for this session.
+ *
+ * @param shuffleMode The shuffle mode. Must be one of the followings:
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
+ * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
+ */
+ public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode);
+
+ /**
+ * Sends a custom action for the {@link MediaSessionCompat} to perform.
+ *
+ * @param customAction The action to perform.
+ * @param args Optional arguments to supply to the
+ * {@link MediaSessionCompat} for this custom action.
+ */
+ public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction,
+ Bundle args);
+
+ /**
+ * Sends the id and args from a custom action for the
+ * {@link MediaSessionCompat} to perform.
+ *
+ * @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
+ * Bundle args)
+ * @see MediaSessionCompat#ACTION_FLAG_AS_INAPPROPRIATE
+ * @see MediaSessionCompat#ACTION_SKIP_AD
+ * @see MediaSessionCompat#ACTION_FOLLOW
+ * @see MediaSessionCompat#ACTION_UNFOLLOW
+ * @param action The action identifier of the
+ * {@link PlaybackStateCompat.CustomAction} as specified by
+ * the {@link MediaSessionCompat}.
+ * @param args Optional arguments to supply to the
+ * {@link MediaSessionCompat} for this custom action.
+ */
+ public abstract void sendCustomAction(String action, Bundle args);
+ }
+
+ /**
+ * Holds information about the way volume is handled for this session.
+ */
+ public static final class PlaybackInfo {
+ /**
+ * The session uses local playback.
+ */
+ public static final int PLAYBACK_TYPE_LOCAL = 1;
+ /**
+ * The session uses remote playback.
+ */
+ public static final int PLAYBACK_TYPE_REMOTE = 2;
+
+ private final int mPlaybackType;
+ // TODO update audio stream with AudioAttributes support version
+ private final int mAudioStream;
+ private final int mVolumeControl;
+ private final int mMaxVolume;
+ private final int mCurrentVolume;
+
+ PlaybackInfo(int type, int stream, int control, int max, int current) {
+ mPlaybackType = type;
+ mAudioStream = stream;
+ mVolumeControl = control;
+ mMaxVolume = max;
+ mCurrentVolume = current;
+ }
+
+ /**
+ * Gets the type of volume handling, either local or remote. One of:
+ * <ul>
+ * <li>{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}</li>
+ * <li>{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}</li>
+ * </ul>
+ *
+ * @return The type of volume handling this session is using.
+ */
+ public int getPlaybackType() {
+ return mPlaybackType;
+ }
+
+ /**
+ * Gets the stream this is currently controlling volume on. When the volume
+ * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
+ * have meaning and should be ignored.
+ *
+ * @return The stream this session is playing on.
+ */
+ public int getAudioStream() {
+ // TODO switch to AudioAttributesCompat when it is added.
+ return mAudioStream;
+ }
+
+ /**
+ * Gets the type of volume control that can be used. One of:
+ * <ul>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
+ * </ul>
+ *
+ * @return The type of volume control that may be used with this
+ * session.
+ */
+ public int getVolumeControl() {
+ return mVolumeControl;
+ }
+
+ /**
+ * Gets the maximum volume that may be set for this session.
+ *
+ * @return The maximum allowed volume where this session is playing.
+ */
+ public int getMaxVolume() {
+ return mMaxVolume;
+ }
+
+ /**
+ * Gets the current volume for this session.
+ *
+ * @return The current volume where this session is playing.
+ */
+ public int getCurrentVolume() {
+ return mCurrentVolume;
+ }
+ }
+
+ interface MediaControllerImpl {
+ void registerCallback(Callback callback, Handler handler);
+
+ void unregisterCallback(Callback callback);
+ boolean dispatchMediaButtonEvent(KeyEvent keyEvent);
+ TransportControls getTransportControls();
+ PlaybackStateCompat getPlaybackState();
+ MediaMetadataCompat getMetadata();
+
+ List<QueueItem> getQueue();
+ void addQueueItem(MediaDescriptionCompat description);
+ void addQueueItem(MediaDescriptionCompat description, int index);
+ void removeQueueItem(MediaDescriptionCompat description);
+ CharSequence getQueueTitle();
+ Bundle getExtras();
+ int getRatingType();
+ boolean isCaptioningEnabled();
+ int getRepeatMode();
+ int getShuffleMode();
+ long getFlags();
+ PlaybackInfo getPlaybackInfo();
+ PendingIntent getSessionActivity();
+
+ void setVolumeTo(int value, int flags);
+ void adjustVolume(int direction, int flags);
+ void sendCommand(String command, Bundle params, ResultReceiver cb);
+
+ boolean isSessionReady();
+ String getPackageName();
+ Object getMediaController();
+ }
+
+ static class MediaControllerImplBase implements MediaControllerImpl {
+ private IMediaSession mBinder;
+ private TransportControls mTransportControls;
+
+ public MediaControllerImplBase(MediaSessionCompat.Token token) {
+ mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken());
+ }
+
+ @Override
+ public void registerCallback(Callback callback, Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback may not be null.");
+ }
+ try {
+ mBinder.asBinder().linkToDeath(callback, 0);
+ mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in registerCallback.", e);
+ callback.onSessionDestroyed();
+ }
+ }
+
+ @Override
+ public void unregisterCallback(Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback may not be null.");
+ }
+ try {
+ mBinder.unregisterCallbackListener(
+ (IMediaControllerCallback) callback.mCallbackObj);
+ mBinder.asBinder().unlinkToDeath(callback, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in unregisterCallback.", e);
+ }
+ }
+
+ @Override
+ public boolean dispatchMediaButtonEvent(KeyEvent event) {
+ if (event == null) {
+ throw new IllegalArgumentException("event may not be null.");
+ }
+ try {
+ mBinder.sendMediaButton(event);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in dispatchMediaButtonEvent.", e);
+ }
+ return false;
+ }
+
+ @Override
+ public TransportControls getTransportControls() {
+ if (mTransportControls == null) {
+ mTransportControls = new TransportControlsBase(mBinder);
+ }
+
+ return mTransportControls;
+ }
+
+ @Override
+ public PlaybackStateCompat getPlaybackState() {
+ try {
+ return mBinder.getPlaybackState();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getPlaybackState.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public MediaMetadataCompat getMetadata() {
+ try {
+ return mBinder.getMetadata();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getMetadata.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public List<QueueItem> getQueue() {
+ try {
+ return mBinder.getQueue();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getQueue.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void addQueueItem(MediaDescriptionCompat description) {
+ try {
+ long flags = mBinder.getFlags();
+ if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+ throw new UnsupportedOperationException(
+ "This session doesn't support queue management operations");
+ }
+ mBinder.addQueueItem(description);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in addQueueItem.", e);
+ }
+ }
+
+ @Override
+ public void addQueueItem(MediaDescriptionCompat description, int index) {
+ try {
+ long flags = mBinder.getFlags();
+ if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+ throw new UnsupportedOperationException(
+ "This session doesn't support queue management operations");
+ }
+ mBinder.addQueueItemAt(description, index);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in addQueueItemAt.", e);
+ }
+ }
+
+ @Override
+ public void removeQueueItem(MediaDescriptionCompat description) {
+ try {
+ long flags = mBinder.getFlags();
+ if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+ throw new UnsupportedOperationException(
+ "This session doesn't support queue management operations");
+ }
+ mBinder.removeQueueItem(description);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in removeQueueItem.", e);
+ }
+ }
+
+ @Override
+ public CharSequence getQueueTitle() {
+ try {
+ return mBinder.getQueueTitle();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getQueueTitle.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public Bundle getExtras() {
+ try {
+ return mBinder.getExtras();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getExtras.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public int getRatingType() {
+ try {
+ return mBinder.getRatingType();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getRatingType.", e);
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean isCaptioningEnabled() {
+ try {
+ return mBinder.isCaptioningEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
+ }
+ return false;
+ }
+
+ @Override
+ public int getRepeatMode() {
+ try {
+ return mBinder.getRepeatMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getRepeatMode.", e);
+ }
+ return PlaybackStateCompat.REPEAT_MODE_INVALID;
+ }
+
+ @Override
+ public int getShuffleMode() {
+ try {
+ return mBinder.getShuffleMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getShuffleMode.", e);
+ }
+ return PlaybackStateCompat.SHUFFLE_MODE_INVALID;
+ }
+
+ @Override
+ public long getFlags() {
+ try {
+ return mBinder.getFlags();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getFlags.", e);
+ }
+ return 0;
+ }
+
+ @Override
+ public PlaybackInfo getPlaybackInfo() {
+ try {
+ ParcelableVolumeInfo info = mBinder.getVolumeAttributes();
+ PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream,
+ info.controlType, info.maxVolume, info.currentVolume);
+ return pi;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getPlaybackInfo.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public PendingIntent getSessionActivity() {
+ try {
+ return mBinder.getLaunchPendingIntent();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getSessionActivity.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void setVolumeTo(int value, int flags) {
+ try {
+ mBinder.setVolumeTo(value, flags, null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setVolumeTo.", e);
+ }
+ }
+
+ @Override
+ public void adjustVolume(int direction, int flags) {
+ try {
+ mBinder.adjustVolume(direction, flags, null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in adjustVolume.", e);
+ }
+ }
+
+ @Override
+ public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+ try {
+ mBinder.sendCommand(command, params,
+ new MediaSessionCompat.ResultReceiverWrapper(cb));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in sendCommand.", e);
+ }
+ }
+
+ @Override
+ public boolean isSessionReady() {
+ return true;
+ }
+
+ @Override
+ public String getPackageName() {
+ try {
+ return mBinder.getPackageName();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getPackageName.", e);
+ }
+ return null;
+ }
+
+ @Override
+ public Object getMediaController() {
+ return null;
+ }
+ }
+
+ static class TransportControlsBase extends TransportControls {
+ private IMediaSession mBinder;
+
+ public TransportControlsBase(IMediaSession binder) {
+ mBinder = binder;
+ }
+
+ @Override
+ public void prepare() {
+ try {
+ mBinder.prepare();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in prepare.", e);
+ }
+ }
+
+ @Override
+ public void prepareFromMediaId(String mediaId, Bundle extras) {
+ try {
+ mBinder.prepareFromMediaId(mediaId, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in prepareFromMediaId.", e);
+ }
+ }
+
+ @Override
+ public void prepareFromSearch(String query, Bundle extras) {
+ try {
+ mBinder.prepareFromSearch(query, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in prepareFromSearch.", e);
+ }
+ }
+
+ @Override
+ public void prepareFromUri(Uri uri, Bundle extras) {
+ try {
+ mBinder.prepareFromUri(uri, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in prepareFromUri.", e);
+ }
+ }
+
+ @Override
+ public void play() {
+ try {
+ mBinder.play();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in play.", e);
+ }
+ }
+
+ @Override
+ public void playFromMediaId(String mediaId, Bundle extras) {
+ try {
+ mBinder.playFromMediaId(mediaId, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in playFromMediaId.", e);
+ }
+ }
+
+ @Override
+ public void playFromSearch(String query, Bundle extras) {
+ try {
+ mBinder.playFromSearch(query, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in playFromSearch.", e);
+ }
+ }
+
+ @Override
+ public void playFromUri(Uri uri, Bundle extras) {
+ try {
+ mBinder.playFromUri(uri, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in playFromUri.", e);
+ }
+ }
+
+ @Override
+ public void skipToQueueItem(long id) {
+ try {
+ mBinder.skipToQueueItem(id);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in skipToQueueItem.", e);
+ }
+ }
+
+ @Override
+ public void pause() {
+ try {
+ mBinder.pause();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in pause.", e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ mBinder.stop();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in stop.", e);
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ try {
+ mBinder.seekTo(pos);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in seekTo.", e);
+ }
+ }
+
+ @Override
+ public void fastForward() {
+ try {
+ mBinder.fastForward();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in fastForward.", e);
+ }
+ }
+
+ @Override
+ public void skipToNext() {
+ try {
+ mBinder.next();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in skipToNext.", e);
+ }
+ }
+
+ @Override
+ public void rewind() {
+ try {
+ mBinder.rewind();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in rewind.", e);
+ }
+ }
+
+ @Override
+ public void skipToPrevious() {
+ try {
+ mBinder.previous();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in skipToPrevious.", e);
+ }
+ }
+
+ @Override
+ public void setRating(RatingCompat rating) {
+ try {
+ mBinder.rate(rating);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setRating.", e);
+ }
+ }
+
+ @Override
+ public void setRating(RatingCompat rating, Bundle extras) {
+ try {
+ mBinder.rateWithExtras(rating, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setRating.", e);
+ }
+ }
+
+ @Override
+ public void setCaptioningEnabled(boolean enabled) {
+ try {
+ mBinder.setCaptioningEnabled(enabled);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setCaptioningEnabled.", e);
+ }
+ }
+
+ @Override
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ try {
+ mBinder.setRepeatMode(repeatMode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setRepeatMode.", e);
+ }
+ }
+
+ @Override
+ public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
+ try {
+ mBinder.setShuffleMode(shuffleMode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in setShuffleMode.", e);
+ }
+ }
+
+ @Override
+ public void sendCustomAction(CustomAction customAction, Bundle args) {
+ sendCustomAction(customAction.getAction(), args);
+ }
+
+ @Override
+ public void sendCustomAction(String action, Bundle args) {
+ validateCustomAction(action, args);
+ try {
+ mBinder.sendCustomAction(action, args);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in sendCustomAction.", e);
+ }
+ }
+ }
+
+ @RequiresApi(21)
+ static class MediaControllerImplApi21 implements MediaControllerImpl {
+ protected final Object mControllerObj;
+
+ private final List<Callback> mPendingCallbacks = new ArrayList<>();
+
+ // Extra binder is used for applying the framework change of new APIs and bug fixes
+ // after API 21.
+ private IMediaSession mExtraBinder;
+ private HashMap<Callback, ExtraCallback> mCallbackMap = new HashMap<>();
+
+ public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
+ mControllerObj = MediaControllerCompatApi21.fromToken(context,
+ session.getSessionToken().getToken());
+ mExtraBinder = session.getSessionToken().getExtraBinder();
+ if (mExtraBinder == null) {
+ requestExtraBinder();
+ }
+ }
+
+ public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
+ throws RemoteException {
+ mControllerObj = MediaControllerCompatApi21.fromToken(context,
+ sessionToken.getToken());
+ if (mControllerObj == null) throw new RemoteException();
+ mExtraBinder = sessionToken.getExtraBinder();
+ if (mExtraBinder == null) {
+ requestExtraBinder();
+ }
+ }
+
+ @Override
+ public final void registerCallback(Callback callback, Handler handler) {
+ MediaControllerCompatApi21.registerCallback(
+ mControllerObj, callback.mCallbackObj, handler);
+ if (mExtraBinder != null) {
+ ExtraCallback extraCallback = new ExtraCallback(callback);
+ mCallbackMap.put(callback, extraCallback);
+ callback.mHasExtraCallback = true;
+ try {
+ mExtraBinder.registerCallbackListener(extraCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in registerCallback.", e);
+ }
+ } else {
+ synchronized (mPendingCallbacks) {
+ callback.mHasExtraCallback = false;
+ mPendingCallbacks.add(callback);
+ }
+ }
+ }
+
+ @Override
+ public final void unregisterCallback(Callback callback) {
+ MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
+ if (mExtraBinder != null) {
+ try {
+ ExtraCallback extraCallback = mCallbackMap.remove(callback);
+ if (extraCallback != null) {
+ callback.mHasExtraCallback = false;
+ mExtraBinder.unregisterCallbackListener(extraCallback);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in unregisterCallback.", e);
+ }
+ } else {
+ synchronized (mPendingCallbacks) {
+ mPendingCallbacks.remove(callback);
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchMediaButtonEvent(KeyEvent event) {
+ return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
+ }
+
+ @Override
+ public TransportControls getTransportControls() {
+ Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
+ return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
+ }
+
+ @Override
+ public PlaybackStateCompat getPlaybackState() {
+ if (mExtraBinder != null) {
+ try {
+ return mExtraBinder.getPlaybackState();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getPlaybackState.", e);
+ }
+ }
+ Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
+ return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
+ }
+
+ @Override
+ public MediaMetadataCompat getMetadata() {
+ Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
+ return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
+ }
+
+ @Override
+ public List<QueueItem> getQueue() {
+ List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
+ return queueObjs != null ? QueueItem.fromQueueItemList(queueObjs) : null;
+ }
+
+ @Override
+ public void addQueueItem(MediaDescriptionCompat description) {
+ long flags = getFlags();
+ if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+ throw new UnsupportedOperationException(
+ "This session doesn't support queue management operations");
+ }
+ Bundle params = new Bundle();
+ params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
+ sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null);
+ }
+
+ @Override
+ public void addQueueItem(MediaDescriptionCompat description, int index) {
+ long flags = getFlags();
+ if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+ throw new UnsupportedOperationException(
+ "This session doesn't support queue management operations");
+ }
+ Bundle params = new Bundle();
+ params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
+ params.putInt(COMMAND_ARGUMENT_INDEX, index);
+ sendCommand(COMMAND_ADD_QUEUE_ITEM_AT, params, null);
+ }
+
+ @Override
+ public void removeQueueItem(MediaDescriptionCompat description) {
+ long flags = getFlags();
+ if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
+ throw new UnsupportedOperationException(
+ "This session doesn't support queue management operations");
+ }
+ Bundle params = new Bundle();
+ params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
+ sendCommand(COMMAND_REMOVE_QUEUE_ITEM, params, null);
+ }
+
+ @Override
+ public CharSequence getQueueTitle() {
+ return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
+ }
+
+ @Override
+ public Bundle getExtras() {
+ return MediaControllerCompatApi21.getExtras(mControllerObj);
+ }
+
+ @Override
+ public int getRatingType() {
+ if (android.os.Build.VERSION.SDK_INT < 22 && mExtraBinder != null) {
+ try {
+ return mExtraBinder.getRatingType();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getRatingType.", e);
+ }
+ }
+ return MediaControllerCompatApi21.getRatingType(mControllerObj);
+ }
+
+ @Override
+ public boolean isCaptioningEnabled() {
+ if (mExtraBinder != null) {
+ try {
+ return mExtraBinder.isCaptioningEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getRepeatMode() {
+ if (mExtraBinder != null) {
+ try {
+ return mExtraBinder.getRepeatMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getRepeatMode.", e);
+ }
+ }
+ return PlaybackStateCompat.REPEAT_MODE_INVALID;
+ }
+
+ @Override
+ public int getShuffleMode() {
+ if (mExtraBinder != null) {
+ try {
+ return mExtraBinder.getShuffleMode();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in getShuffleMode.", e);
+ }
+ }
+ return PlaybackStateCompat.SHUFFLE_MODE_INVALID;
+ }
+
+ @Override
+ public long getFlags() {
+ return MediaControllerCompatApi21.getFlags(mControllerObj);
+ }
+
+ @Override
+ public PlaybackInfo getPlaybackInfo() {
+ Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj);
+ return volumeInfoObj != null ? new PlaybackInfo(
+ MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj),
+ MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj),
+ MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj),
+ MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj),
+ MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null;
+ }
+
+ @Override
+ public PendingIntent getSessionActivity() {
+ return MediaControllerCompatApi21.getSessionActivity(mControllerObj);
+ }
+
+ @Override
+ public void setVolumeTo(int value, int flags) {
+ MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags);
+ }
+
+ @Override
+ public void adjustVolume(int direction, int flags) {
+ MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags);
+ }
+
+ @Override
+ public void sendCommand(String command, Bundle params, ResultReceiver cb) {
+ MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb);
+ }
+
+ @Override
+ public boolean isSessionReady() {
+ return mExtraBinder != null;
+ }
+
+ @Override
+ public String getPackageName() {
+ return MediaControllerCompatApi21.getPackageName(mControllerObj);
+ }
+
+ @Override
+ public Object getMediaController() {
+ return mControllerObj;
+ }
+
+ private void requestExtraBinder() {
+ sendCommand(COMMAND_GET_EXTRA_BINDER, null,
+ new ExtraBinderRequestResultReceiver(this, new Handler()));
+ }
+
+ private void processPendingCallbacks() {
+ if (mExtraBinder == null) {
+ return;
+ }
+ synchronized (mPendingCallbacks) {
+ for (Callback callback : mPendingCallbacks) {
+ ExtraCallback extraCallback = new ExtraCallback(callback);
+ mCallbackMap.put(callback, extraCallback);
+ callback.mHasExtraCallback = true;
+ try {
+ mExtraBinder.registerCallbackListener(extraCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in registerCallback.", e);
+ break;
+ }
+ callback.onSessionReady();
+ }
+ mPendingCallbacks.clear();
+ }
+ }
+
+ private static class ExtraBinderRequestResultReceiver extends ResultReceiver {
+ private WeakReference<MediaControllerImplApi21> mMediaControllerImpl;
+
+ public ExtraBinderRequestResultReceiver(MediaControllerImplApi21 mediaControllerImpl,
+ Handler handler) {
+ super(handler);
+ mMediaControllerImpl = new WeakReference<>(mediaControllerImpl);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ MediaControllerImplApi21 mediaControllerImpl = mMediaControllerImpl.get();
+ if (mediaControllerImpl == null || resultData == null) {
+ return;
+ }
+ mediaControllerImpl.mExtraBinder = IMediaSession.Stub.asInterface(
+ BundleCompat.getBinder(resultData, MediaSessionCompat.EXTRA_BINDER));
+ mediaControllerImpl.processPendingCallbacks();
+ }
+ }
+
+ private static class ExtraCallback extends Callback.StubCompat {
+ ExtraCallback(Callback callback) {
+ super(callback);
+ }
+
+ @Override
+ public void onSessionDestroyed() throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
+ public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
+ public void onQueueTitleChanged(CharSequence title) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+
+ @Override
+ public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
+ // Will not be called.
+ throw new AssertionError();
+ }
+ }
+ }
+
+ static class TransportControlsApi21 extends TransportControls {
+ protected final Object mControlsObj;
+
+ public TransportControlsApi21(Object controlsObj) {
+ mControlsObj = controlsObj;
+ }
+
+ @Override
+ public void prepare() {
+ sendCustomAction(MediaSessionCompat.ACTION_PREPARE, null);
+ }
+
+ @Override
+ public void prepareFromMediaId(String mediaId, Bundle extras) {
+ Bundle bundle = new Bundle();
+ bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ID, mediaId);
+ bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
+ sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_MEDIA_ID, bundle);
+ }
+
+ @Override
+ public void prepareFromSearch(String query, Bundle extras) {
+ Bundle bundle = new Bundle();
+ bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_QUERY, query);
+ bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
+ sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_SEARCH, bundle);
+ }
+
+ @Override
+ public void prepareFromUri(Uri uri, Bundle extras) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
+ bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
+ sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_URI, bundle);
+ }
+
+ @Override
+ public void play() {
+ MediaControllerCompatApi21.TransportControls.play(mControlsObj);
+ }
+
+ @Override
+ public void pause() {
+ MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
+ }
+
+ @Override
+ public void stop() {
+ MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
+ }
+
+ @Override
+ public void fastForward() {
+ MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
+ }
+
+ @Override
+ public void rewind() {
+ MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
+ }
+
+ @Override
+ public void skipToNext() {
+ MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
+ }
+
+ @Override
+ public void skipToPrevious() {
+ MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
+ }
+
+ @Override
+ public void setRating(RatingCompat rating) {
+ MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
+ rating != null ? rating.getRating() : null);
+ }
+
+ @Override
+ public void setRating(RatingCompat rating, Bundle extras) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_RATING, rating);
+ bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
+ sendCustomAction(MediaSessionCompat.ACTION_SET_RATING, bundle);
+ }
+
+ @Override
+ public void setCaptioningEnabled(boolean enabled) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_CAPTIONING_ENABLED, enabled);
+ sendCustomAction(MediaSessionCompat.ACTION_SET_CAPTIONING_ENABLED, bundle);
+ }
+
+ @Override
+ public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
+ sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
+ }
+
+ @Override
+ public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE, shuffleMode);
+ sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE, bundle);
+ }
+
+ @Override
+ public void playFromMediaId(String mediaId, Bundle extras) {
+ MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
+ extras);
+ }
+
+ @Override
+ public void playFromSearch(String query, Bundle extras) {
+ MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query,
+ extras);
+ }
+
+ @Override
+ public void playFromUri(Uri uri, Bundle extras) {
+ if (uri == null || Uri.EMPTY.equals(uri)) {
+ throw new IllegalArgumentException(
+ "You must specify a non-empty Uri for playFromUri.");
+ }
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
+ bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
+ sendCustomAction(MediaSessionCompat.ACTION_PLAY_FROM_URI, bundle);
+ }
+
+ @Override
+ public void skipToQueueItem(long id) {
+ MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id);
+ }
+
+ @Override
+ public void sendCustomAction(CustomAction customAction, Bundle args) {
+ validateCustomAction(customAction.getAction(), args);
+ MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
+ customAction.getAction(), args);
+ }
+
+ @Override
+ public void sendCustomAction(String action, Bundle args) {
+ validateCustomAction(action, args);
+ MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action,
+ args);
+ }
+ }
+
+ @RequiresApi(23)
+ static class MediaControllerImplApi23 extends MediaControllerImplApi21 {
+
+ public MediaControllerImplApi23(Context context, MediaSessionCompat session) {
+ super(context, session);
+ }
+
+ public MediaControllerImplApi23(Context context, MediaSessionCompat.Token sessionToken)
+ throws RemoteException {
+ super(context, sessionToken);
+ }
+
+ @Override
+ public TransportControls getTransportControls() {
+ Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
+ return controlsObj != null ? new TransportControlsApi23(controlsObj) : null;
+ }
+ }
+
+ @RequiresApi(23)
+ static class TransportControlsApi23 extends TransportControlsApi21 {
+
+ public TransportControlsApi23(Object controlsObj) {
+ super(controlsObj);
+ }
+
+ @Override
+ public void playFromUri(Uri uri, Bundle extras) {
+ MediaControllerCompatApi23.TransportControls.playFromUri(mControlsObj, uri,
+ extras);
+ }
+ }
+
+ @RequiresApi(24)
+ static class MediaControllerImplApi24 extends MediaControllerImplApi23 {
+
+ public MediaControllerImplApi24(Context context, MediaSessionCompat session) {
+ super(context, session);
+ }
+
+ public MediaControllerImplApi24(Context context, MediaSessionCompat.Token sessionToken)
+ throws RemoteException {
+ super(context, sessionToken);
+ }
+
+ @Override
+ public TransportControls getTransportControls() {
+ Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
+ return controlsObj != null ? new TransportControlsApi24(controlsObj) : null;
+ }
+ }
+
+ @RequiresApi(24)
+ static class TransportControlsApi24 extends TransportControlsApi23 {
+
+ public TransportControlsApi24(Object controlsObj) {
+ super(controlsObj);
+ }
+
+ @Override
+ public void prepare() {
+ MediaControllerCompatApi24.TransportControls.prepare(mControlsObj);
+ }
+
+ @Override
+ public void prepareFromMediaId(String mediaId, Bundle extras) {
+ MediaControllerCompatApi24.TransportControls.prepareFromMediaId(
+ mControlsObj, mediaId, extras);
+ }
+
+ @Override
+ public void prepareFromSearch(String query, Bundle extras) {
+ MediaControllerCompatApi24.TransportControls.prepareFromSearch(
+ mControlsObj, query, extras);
+ }
+
+ @Override
+ public void prepareFromUri(Uri uri, Bundle extras) {
+ MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
+ }
+ }
+}
diff --git a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index fb939c0..1d78fc8 100644
--- a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -60,7 +60,6 @@
import androidx.core.app.BundleCompat;
import androidx.media.VolumeProviderCompat;
import androidx.media.session.MediaButtonReceiver;
-import androidx.media.session.MediaControllerCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java b/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
index c2a9741..e6420ea 100644
--- a/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -30,7 +30,6 @@
import androidx.annotation.LongDef;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.media.session.MediaControllerCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/media/src/main/java/androidx/media/MediaBrowserCompat.java b/media/src/main/java/androidx/media/MediaBrowserCompat.java
deleted file mode 100644
index 05123dc..0000000
--- a/media/src/main/java/androidx/media/MediaBrowserCompat.java
+++ /dev/null
@@ -1,2289 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
-import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
-import static androidx.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
-import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
-import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
-import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
-import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
-import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
-import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
-import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
-import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
-import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
-import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
-import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
-import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
-import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
-import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
-import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
-import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
-import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
-import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
-import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
-import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_2;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.BadParcelableException;
-import android.os.Binder;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.os.RemoteException;
-import android.support.v4.media.MediaDescriptionCompat;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.session.IMediaSession;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.os.ResultReceiver;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.collection.ArrayMap;
-import androidx.core.app.BundleCompat;
-import androidx.media.session.MediaControllerCompat.TransportControls;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Browses media content offered by a {@link MediaBrowserServiceCompat}.
- * <p>
- * This object is not thread-safe. All calls should happen on the thread on which the browser
- * was constructed.
- * </p><p>
- * All callback methods will be called from the thread on which the browser was constructed.
- * </p>
- *
- * <div class="special reference">
- * <h3>Developer Guides</h3>
- * <p>For information about building your media application, read the
- * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
- * </div>
- */
-public final class MediaBrowserCompat {
- static final String TAG = "MediaBrowserCompat";
- static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
- /**
- * Used as an int extra field to denote the page number to subscribe.
- * The value of {@code EXTRA_PAGE} should be greater than or equal to 1.
- *
- * @see android.service.media.MediaBrowserService.BrowserRoot
- * @see #EXTRA_PAGE_SIZE
- */
- public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE";
-
- /**
- * Used as an int extra field to denote the number of media items in a page.
- * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1.
- *
- * @see android.service.media.MediaBrowserService.BrowserRoot
- * @see #EXTRA_PAGE
- */
- public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
-
- /**
- * Used as a string extra field to denote the target {@link MediaItem}.
- *
- * @see #CUSTOM_ACTION_DOWNLOAD
- * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
- */
- public static final String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID";
-
- /**
- * Used as a float extra field to denote the current progress during download. The value of this
- * field must be a float number within [0.0, 1.0].
- *
- * @see #CUSTOM_ACTION_DOWNLOAD
- * @see CustomActionCallback#onProgressUpdate
- */
- public static final String EXTRA_DOWNLOAD_PROGRESS =
- "android.media.browse.extra.DOWNLOAD_PROGRESS";
-
- /**
- * Predefined custom action to ask the connected service to download a specific
- * {@link MediaItem} for offline playback. The id of the media item must be passed in an extra
- * bundle. The download progress might be delivered to the browser via
- * {@link CustomActionCallback#onProgressUpdate}.
- *
- * @see #EXTRA_MEDIA_ID
- * @see #EXTRA_DOWNLOAD_PROGRESS
- * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
- */
- public static final String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD";
-
- /**
- * Predefined custom action to ask the connected service to remove the downloaded file of
- * {@link MediaItem} by the {@link #CUSTOM_ACTION_DOWNLOAD download} action. The id of the
- * media item must be passed in an extra bundle.
- *
- * @see #EXTRA_MEDIA_ID
- * @see #CUSTOM_ACTION_DOWNLOAD
- */
- public static final String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE =
- "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE";
-
- private final MediaBrowserImpl mImpl;
-
- /**
- * Creates a media browser for the specified media browse service.
- *
- * @param context The context.
- * @param serviceComponent The component name of the media browse service.
- * @param callback The connection callback.
- * @param rootHints An optional bundle of service-specific arguments to send
- * to the media browse service when connecting and retrieving the root id
- * for browsing, or null if none. The contents of this bundle may affect
- * the information returned when browsing.
- * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
- * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
- * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
- */
- public MediaBrowserCompat(Context context, ComponentName serviceComponent,
- ConnectionCallback callback, Bundle rootHints) {
- // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
- // and 25 devices, use the support library version of implementation on those devices.
- if (Build.VERSION.SDK_INT >= 26) {
- mImpl = new MediaBrowserImplApi26(context, serviceComponent, callback, rootHints);
- } else if (Build.VERSION.SDK_INT >= 23) {
- mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
- } else if (Build.VERSION.SDK_INT >= 21) {
- mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints);
- } else {
- mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints);
- }
- }
-
- /**
- * Connects to the media browse service.
- * <p>
- * The connection callback specified in the constructor will be invoked
- * when the connection completes or fails.
- * </p>
- */
- public void connect() {
- mImpl.connect();
- }
-
- /**
- * Disconnects from the media browse service.
- * After this, no more callbacks will be received.
- */
- public void disconnect() {
- mImpl.disconnect();
- }
-
- /**
- * Returns whether the browser is connected to the service.
- */
- public boolean isConnected() {
- return mImpl.isConnected();
- }
-
- /**
- * Gets the service component that the media browser is connected to.
- */
- public @NonNull
- ComponentName getServiceComponent() {
- return mImpl.getServiceComponent();
- }
-
- /**
- * Gets the root id.
- * <p>
- * Note that the root id may become invalid or change when when the
- * browser is disconnected.
- * </p>
- *
- * @throws IllegalStateException if not connected.
- */
- public @NonNull String getRoot() {
- return mImpl.getRoot();
- }
-
- /**
- * Gets any extras for the media service.
- *
- * @throws IllegalStateException if not connected.
- */
- public @Nullable
- Bundle getExtras() {
- return mImpl.getExtras();
- }
-
- /**
- * Gets the media session token associated with the media browser.
- * <p>
- * Note that the session token may become invalid or change when when the
- * browser is disconnected.
- * </p>
- *
- * @return The session token for the browser, never null.
- *
- * @throws IllegalStateException if not connected.
- */
- public @NonNull MediaSessionCompat.Token getSessionToken() {
- return mImpl.getSessionToken();
- }
-
- /**
- * Queries for information about the media items that are contained within
- * the specified id and subscribes to receive updates when they change.
- * <p>
- * The list of subscriptions is maintained even when not connected and is
- * restored after the reconnection. It is ok to subscribe while not connected
- * but the results will not be returned until the connection completes.
- * </p>
- * <p>
- * If the id is already subscribed with a different callback then the new
- * callback will replace the previous one and the child data will be
- * reloaded.
- * </p>
- *
- * @param parentId The id of the parent media item whose list of children
- * will be subscribed.
- * @param callback The callback to receive the list of children.
- */
- public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
- // Check arguments.
- if (TextUtils.isEmpty(parentId)) {
- throw new IllegalArgumentException("parentId is empty");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback is null");
- }
- mImpl.subscribe(parentId, null, callback);
- }
-
- /**
- * Queries with service-specific arguments for information about the media items
- * that are contained within the specified id and subscribes to receive updates
- * when they change.
- * <p>
- * The list of subscriptions is maintained even when not connected and is
- * restored after the reconnection. It is ok to subscribe while not connected
- * but the results will not be returned until the connection completes.
- * </p>
- * <p>
- * If the id is already subscribed with a different callback then the new
- * callback will replace the previous one and the child data will be
- * reloaded.
- * </p>
- *
- * @param parentId The id of the parent media item whose list of children
- * will be subscribed.
- * @param options A bundle of service-specific arguments to send to the media
- * browse service. The contents of this bundle may affect the
- * information returned when browsing.
- * @param callback The callback to receive the list of children.
- */
- public void subscribe(@NonNull String parentId, @NonNull Bundle options,
- @NonNull SubscriptionCallback callback) {
- // Check arguments.
- if (TextUtils.isEmpty(parentId)) {
- throw new IllegalArgumentException("parentId is empty");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback is null");
- }
- if (options == null) {
- throw new IllegalArgumentException("options are null");
- }
- mImpl.subscribe(parentId, options, callback);
- }
-
- /**
- * Unsubscribes for changes to the children of the specified media id.
- * <p>
- * The query callback will no longer be invoked for results associated with
- * this id once this method returns.
- * </p>
- *
- * @param parentId The id of the parent media item whose list of children
- * will be unsubscribed.
- */
- public void unsubscribe(@NonNull String parentId) {
- // Check arguments.
- if (TextUtils.isEmpty(parentId)) {
- throw new IllegalArgumentException("parentId is empty");
- }
- mImpl.unsubscribe(parentId, null);
- }
-
- /**
- * Unsubscribes for changes to the children of the specified media id.
- * <p>
- * The query callback will no longer be invoked for results associated with
- * this id once this method returns.
- * </p>
- *
- * @param parentId The id of the parent media item whose list of children
- * will be unsubscribed.
- * @param callback A callback sent to the media browse service to subscribe.
- */
- public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
- // Check arguments.
- if (TextUtils.isEmpty(parentId)) {
- throw new IllegalArgumentException("parentId is empty");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback is null");
- }
- mImpl.unsubscribe(parentId, callback);
- }
-
- /**
- * Retrieves a specific {@link MediaItem} from the connected service. Not
- * all services may support this, so falling back to subscribing to the
- * parent's id should be used when unavailable.
- *
- * @param mediaId The id of the item to retrieve.
- * @param cb The callback to receive the result on.
- */
- public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
- mImpl.getItem(mediaId, cb);
- }
-
- /**
- * Searches {@link MediaItem media items} from the connected service. Not all services may
- * support this, and {@link SearchCallback#onError} will be called if not implemented.
- *
- * @param query The search query that contains keywords separated by space. Should not be an
- * empty string.
- * @param extras The bundle of service-specific arguments to send to the media browser service.
- * The contents of this bundle may affect the search result.
- * @param callback The callback to receive the search result. Must be non-null.
- * @throws IllegalStateException if the browser is not connected to the media browser service.
- */
- public void search(@NonNull final String query, final Bundle extras,
- @NonNull SearchCallback callback) {
- if (TextUtils.isEmpty(query)) {
- throw new IllegalArgumentException("query cannot be empty");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback cannot be null");
- }
- mImpl.search(query, extras, callback);
- }
-
- /**
- * Sends a custom action to the connected service. If the service doesn't support the given
- * action, {@link CustomActionCallback#onError} will be called.
- *
- * @param action The custom action that will be sent to the connected service. Should not be an
- * empty string.
- * @param extras The bundle of service-specific arguments to send to the media browser service.
- * @param callback The callback to receive the result of the custom action.
- * @see #CUSTOM_ACTION_DOWNLOAD
- * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
- */
- public void sendCustomAction(@NonNull String action, Bundle extras,
- @Nullable CustomActionCallback callback) {
- if (TextUtils.isEmpty(action)) {
- throw new IllegalArgumentException("action cannot be empty");
- }
- mImpl.sendCustomAction(action, extras, callback);
- }
-
- /**
- * A class with information on a single media item for use in browsing/searching media.
- * MediaItems are application dependent so we cannot guarantee that they contain the
- * right values.
- */
- public static class MediaItem implements Parcelable {
- private final int mFlags;
- private final MediaDescriptionCompat mDescription;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
- public @interface Flags { }
-
- /**
- * Flag: Indicates that the item has children of its own.
- */
- public static final int FLAG_BROWSABLE = 1 << 0;
-
- /**
- * Flag: Indicates that the item is playable.
- * <p>
- * The id of this item may be passed to
- * {@link TransportControls#playFromMediaId(String, Bundle)}
- * to start playing it.
- * </p>
- */
- public static final int FLAG_PLAYABLE = 1 << 1;
-
- /**
- * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem}
- * object.
- * <p>
- * This method is only supported on API 21+. On API 20 and below, it returns null.
- * </p>
- *
- * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object.
- * @return An equivalent {@link MediaItem} object, or null if none.
- */
- public static MediaItem fromMediaItem(Object itemObj) {
- if (itemObj == null || Build.VERSION.SDK_INT < 21) {
- return null;
- }
- int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj);
- MediaDescriptionCompat description =
- MediaDescriptionCompat.fromMediaDescription(
- MediaBrowserCompatApi21.MediaItem.getDescription(itemObj));
- return new MediaItem(description, flags);
- }
-
- /**
- * Creates a list of {@link MediaItem} objects from a framework
- * {@link android.media.browse.MediaBrowser.MediaItem} object list.
- * <p>
- * This method is only supported on API 21+. On API 20 and below, it returns null.
- * </p>
- *
- * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects.
- * @return An equivalent list of {@link MediaItem} objects, or null if none.
- */
- public static List<MediaItem> fromMediaItemList(List<?> itemList) {
- if (itemList == null || Build.VERSION.SDK_INT < 21) {
- return null;
- }
- List<MediaItem> items = new ArrayList<>(itemList.size());
- for (Object itemObj : itemList) {
- items.add(fromMediaItem(itemObj));
- }
- return items;
- }
-
- /**
- * Create a new MediaItem for use in browsing media.
- * @param description The description of the media, which must include a
- * media id.
- * @param flags The flags for this item.
- */
- public MediaItem(@NonNull MediaDescriptionCompat description, @Flags int flags) {
- if (description == null) {
- throw new IllegalArgumentException("description cannot be null");
- }
- if (TextUtils.isEmpty(description.getMediaId())) {
- throw new IllegalArgumentException("description must have a non-empty media id");
- }
- mFlags = flags;
- mDescription = description;
- }
-
- /**
- * Private constructor.
- */
- MediaItem(Parcel in) {
- mFlags = in.readInt();
- mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(mFlags);
- mDescription.writeToParcel(out, flags);
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder("MediaItem{");
- sb.append("mFlags=").append(mFlags);
- sb.append(", mDescription=").append(mDescription);
- sb.append('}');
- return sb.toString();
- }
-
- public static final Parcelable.Creator<MediaItem> CREATOR =
- new Parcelable.Creator<MediaItem>() {
- @Override
- public MediaItem createFromParcel(Parcel in) {
- return new MediaItem(in);
- }
-
- @Override
- public MediaItem[] newArray(int size) {
- return new MediaItem[size];
- }
- };
-
- /**
- * Gets the flags of the item.
- */
- public @Flags int getFlags() {
- return mFlags;
- }
-
- /**
- * Returns whether this item is browsable.
- * @see #FLAG_BROWSABLE
- */
- public boolean isBrowsable() {
- return (mFlags & FLAG_BROWSABLE) != 0;
- }
-
- /**
- * Returns whether this item is playable.
- * @see #FLAG_PLAYABLE
- */
- public boolean isPlayable() {
- return (mFlags & FLAG_PLAYABLE) != 0;
- }
-
- /**
- * Returns the description of the media.
- */
- public @NonNull MediaDescriptionCompat getDescription() {
- return mDescription;
- }
-
- /**
- * Returns the media id in the {@link MediaDescriptionCompat} for this item.
- * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
- */
- public @Nullable String getMediaId() {
- return mDescription.getMediaId();
- }
- }
-
- /**
- * Callbacks for connection related events.
- */
- public static class ConnectionCallback {
- final Object mConnectionCallbackObj;
- ConnectionCallbackInternal mConnectionCallbackInternal;
-
- public ConnectionCallback() {
- if (Build.VERSION.SDK_INT >= 21) {
- mConnectionCallbackObj =
- MediaBrowserCompatApi21.createConnectionCallback(new StubApi21());
- } else {
- mConnectionCallbackObj = null;
- }
- }
-
- /**
- * Invoked after {@link MediaBrowserCompat#connect()} when the request has successfully
- * completed.
- */
- public void onConnected() {
- }
-
- /**
- * Invoked when the client is disconnected from the media browser.
- */
- public void onConnectionSuspended() {
- }
-
- /**
- * Invoked when the connection to the media browser failed.
- */
- public void onConnectionFailed() {
- }
-
- void setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal) {
- mConnectionCallbackInternal = connectionCallbackInternal;
- }
-
- interface ConnectionCallbackInternal {
- void onConnected();
- void onConnectionSuspended();
- void onConnectionFailed();
- }
-
- private class StubApi21 implements MediaBrowserCompatApi21.ConnectionCallback {
- StubApi21() {
- }
-
- @Override
- public void onConnected() {
- if (mConnectionCallbackInternal != null) {
- mConnectionCallbackInternal.onConnected();
- }
- ConnectionCallback.this.onConnected();
- }
-
- @Override
- public void onConnectionSuspended() {
- if (mConnectionCallbackInternal != null) {
- mConnectionCallbackInternal.onConnectionSuspended();
- }
- ConnectionCallback.this.onConnectionSuspended();
- }
-
- @Override
- public void onConnectionFailed() {
- if (mConnectionCallbackInternal != null) {
- mConnectionCallbackInternal.onConnectionFailed();
- }
- ConnectionCallback.this.onConnectionFailed();
- }
- }
- }
-
- /**
- * Callbacks for subscription related events.
- */
- public static abstract class SubscriptionCallback {
- private final Object mSubscriptionCallbackObj;
- private final IBinder mToken;
- WeakReference<Subscription> mSubscriptionRef;
-
- public SubscriptionCallback() {
- mToken = new Binder();
- if (Build.VERSION.SDK_INT >= 26) {
- mSubscriptionCallbackObj =
- MediaBrowserCompatApi26.createSubscriptionCallback(new StubApi26());
- } else if (Build.VERSION.SDK_INT >= 21) {
- mSubscriptionCallbackObj =
- MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
- } else {
- mSubscriptionCallbackObj = null;
- }
- }
-
- /**
- * Called when the list of children is loaded or updated.
- *
- * @param parentId The media id of the parent media item.
- * @param children The children which were loaded.
- */
- public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
- }
-
- /**
- * Called when the list of children is loaded or updated.
- *
- * @param parentId The media id of the parent media item.
- * @param children The children which were loaded.
- * @param options A bundle of service-specific arguments to send to the media
- * browse service. The contents of this bundle may affect the
- * information returned when browsing.
- */
- public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
- @NonNull Bundle options) {
- }
-
- /**
- * Called when the id doesn't exist or other errors in subscribing.
- * <p>
- * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe}
- * called, because some errors may heal themselves.
- * </p>
- *
- * @param parentId The media id of the parent media item whose children could not be loaded.
- */
- public void onError(@NonNull String parentId) {
- }
-
- /**
- * Called when the id doesn't exist or other errors in subscribing.
- * <p>
- * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe}
- * called, because some errors may heal themselves.
- * </p>
- *
- * @param parentId The media id of the parent media item whose children could
- * not be loaded.
- * @param options A bundle of service-specific arguments sent to the media
- * browse service.
- */
- public void onError(@NonNull String parentId, @NonNull Bundle options) {
- }
-
- private void setSubscription(Subscription subscription) {
- mSubscriptionRef = new WeakReference<>(subscription);
- }
-
- private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback {
- StubApi21() {
- }
-
- @Override
- public void onChildrenLoaded(@NonNull String parentId, List<?> children) {
- Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get();
- if (sub == null) {
- SubscriptionCallback.this.onChildrenLoaded(
- parentId, MediaItem.fromMediaItemList(children));
- } else {
- List<MediaBrowserCompat.MediaItem> itemList =
- MediaItem.fromMediaItemList(children);
- final List<SubscriptionCallback> callbacks = sub.getCallbacks();
- final List<Bundle> optionsList = sub.getOptionsList();
- for (int i = 0; i < callbacks.size(); ++i) {
- Bundle options = optionsList.get(i);
- if (options == null) {
- SubscriptionCallback.this.onChildrenLoaded(parentId, itemList);
- } else {
- SubscriptionCallback.this.onChildrenLoaded(
- parentId, applyOptions(itemList, options), options);
- }
- }
- }
- }
-
- @Override
- public void onError(@NonNull String parentId) {
- SubscriptionCallback.this.onError(parentId);
- }
-
- List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
- final Bundle options) {
- if (list == null) {
- return null;
- }
- int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
- int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
- if (page == -1 && pageSize == -1) {
- return list;
- }
- int fromIndex = pageSize * page;
- int toIndex = fromIndex + pageSize;
- if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
- return Collections.EMPTY_LIST;
- }
- if (toIndex > list.size()) {
- toIndex = list.size();
- }
- return list.subList(fromIndex, toIndex);
- }
-
- }
-
- private class StubApi26 extends StubApi21
- implements MediaBrowserCompatApi26.SubscriptionCallback {
- StubApi26() {
- }
-
- @Override
- public void onChildrenLoaded(@NonNull String parentId, List<?> children,
- @NonNull Bundle options) {
- SubscriptionCallback.this.onChildrenLoaded(
- parentId, MediaItem.fromMediaItemList(children), options);
- }
-
- @Override
- public void onError(@NonNull String parentId, @NonNull Bundle options) {
- SubscriptionCallback.this.onError(parentId, options);
- }
- }
- }
-
- /**
- * Callback for receiving the result of {@link #getItem}.
- */
- public static abstract class ItemCallback {
- final Object mItemCallbackObj;
-
- public ItemCallback() {
- if (Build.VERSION.SDK_INT >= 23) {
- mItemCallbackObj = MediaBrowserCompatApi23.createItemCallback(new StubApi23());
- } else {
- mItemCallbackObj = null;
- }
- }
-
- /**
- * Called when the item has been returned by the browser service.
- *
- * @param item The item that was returned or null if it doesn't exist.
- */
- public void onItemLoaded(MediaItem item) {
- }
-
- /**
- * Called when the item doesn't exist or there was an error retrieving it.
- *
- * @param itemId The media id of the media item which could not be loaded.
- */
- public void onError(@NonNull String itemId) {
- }
-
- private class StubApi23 implements MediaBrowserCompatApi23.ItemCallback {
- StubApi23() {
- }
-
- @Override
- public void onItemLoaded(Parcel itemParcel) {
- if (itemParcel == null) {
- ItemCallback.this.onItemLoaded(null);
- } else {
- itemParcel.setDataPosition(0);
- MediaItem item =
- MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
- itemParcel.recycle();
- ItemCallback.this.onItemLoaded(item);
- }
- }
-
- @Override
- public void onError(@NonNull String itemId) {
- ItemCallback.this.onError(itemId);
- }
- }
- }
-
- /**
- * Callback for receiving the result of {@link #search}.
- */
- public abstract static class SearchCallback {
- /**
- * Called when the {@link #search} finished successfully.
- *
- * @param query The search query sent for the search request to the connected service.
- * @param extras The bundle of service-specific arguments sent to the connected service.
- * @param items The list of media items which contains the search result.
- */
- public void onSearchResult(@NonNull String query, Bundle extras,
- @NonNull List<MediaItem> items) {
- }
-
- /**
- * Called when an error happens while {@link #search} or the connected service doesn't
- * support {@link #search}.
- *
- * @param query The search query sent for the search request to the connected service.
- * @param extras The bundle of service-specific arguments sent to the connected service.
- */
- public void onError(@NonNull String query, Bundle extras) {
- }
- }
-
- /**
- * Callback for receiving the result of {@link #sendCustomAction}.
- */
- public abstract static class CustomActionCallback {
- /**
- * Called when an interim update was delivered from the connected service while performing
- * the custom action.
- *
- * @param action The custom action sent to the connected service.
- * @param extras The bundle of service-specific arguments sent to the connected service.
- * @param data The additional data delivered from the connected service.
- */
- public void onProgressUpdate(String action, Bundle extras, Bundle data) {
- }
-
- /**
- * Called when the custom action finished successfully.
- *
- * @param action The custom action sent to the connected service.
- * @param extras The bundle of service-specific arguments sent to the connected service.
- * @param resultData The additional data delivered from the connected service.
- */
- public void onResult(String action, Bundle extras, Bundle resultData) {
- }
-
- /**
- * Called when an error happens while performing the custom action or the connected service
- * doesn't support the requested custom action.
- *
- * @param action The custom action sent to the connected service.
- * @param extras The bundle of service-specific arguments sent to the connected service.
- * @param data The additional data delivered from the connected service.
- */
- public void onError(String action, Bundle extras, Bundle data) {
- }
- }
-
- interface MediaBrowserImpl {
- void connect();
- void disconnect();
- boolean isConnected();
- ComponentName getServiceComponent();
- @NonNull String getRoot();
- @Nullable Bundle getExtras();
- @NonNull MediaSessionCompat.Token getSessionToken();
- void subscribe(@NonNull String parentId, @Nullable Bundle options,
- @NonNull SubscriptionCallback callback);
- void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
- void getItem(@NonNull String mediaId, @NonNull ItemCallback cb);
- void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
- void sendCustomAction(@NonNull String action, Bundle extras,
- @Nullable CustomActionCallback callback);
- }
-
- interface MediaBrowserServiceCallbackImpl {
- void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session,
- Bundle extra);
- void onConnectionFailed(Messenger callback);
- void onLoadChildren(Messenger callback, String parentId, List list, Bundle options);
- }
-
- static class MediaBrowserImplBase
- implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl {
- static final int CONNECT_STATE_DISCONNECTING = 0;
- static final int CONNECT_STATE_DISCONNECTED = 1;
- static final int CONNECT_STATE_CONNECTING = 2;
- static final int CONNECT_STATE_CONNECTED = 3;
- static final int CONNECT_STATE_SUSPENDED = 4;
-
- final Context mContext;
- final ComponentName mServiceComponent;
- final ConnectionCallback mCallback;
- final Bundle mRootHints;
- final CallbackHandler mHandler = new CallbackHandler(this);
- private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
-
- int mState = CONNECT_STATE_DISCONNECTED;
- MediaServiceConnection mServiceConnection;
- ServiceBinderWrapper mServiceBinderWrapper;
- Messenger mCallbacksMessenger;
- private String mRootId;
- private MediaSessionCompat.Token mMediaSessionToken;
- private Bundle mExtras;
-
- public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
- ConnectionCallback callback, Bundle rootHints) {
- if (context == null) {
- throw new IllegalArgumentException("context must not be null");
- }
- if (serviceComponent == null) {
- throw new IllegalArgumentException("service component must not be null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("connection callback must not be null");
- }
- mContext = context;
- mServiceComponent = serviceComponent;
- mCallback = callback;
- mRootHints = rootHints == null ? null : new Bundle(rootHints);
- }
-
- @Override
- public void connect() {
- if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
- throw new IllegalStateException("connect() called while neigther disconnecting nor "
- + "disconnected (state=" + getStateLabel(mState) + ")");
- }
-
- mState = CONNECT_STATE_CONNECTING;
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // mState could be changed by the Runnable of disconnect()
- if (mState == CONNECT_STATE_DISCONNECTING) {
- return;
- }
- mState = CONNECT_STATE_CONNECTING;
- // TODO: remove this extra check.
- if (DEBUG) {
- if (mServiceConnection != null) {
- throw new RuntimeException("mServiceConnection should be null. Instead "
- + "it is " + mServiceConnection);
- }
- }
- if (mServiceBinderWrapper != null) {
- throw new RuntimeException("mServiceBinderWrapper should be null. Instead "
- + "it is " + mServiceBinderWrapper);
- }
- if (mCallbacksMessenger != null) {
- throw new RuntimeException("mCallbacksMessenger should be null. Instead "
- + "it is " + mCallbacksMessenger);
- }
-
- final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
- intent.setComponent(mServiceComponent);
-
- mServiceConnection = new MediaServiceConnection();
- boolean bound = false;
- try {
- bound = mContext.bindService(intent, mServiceConnection,
- Context.BIND_AUTO_CREATE);
- } catch (Exception ex) {
- Log.e(TAG, "Failed binding to service " + mServiceComponent);
- }
-
- if (!bound) {
- // Tell them that it didn't work.
- forceCloseConnection();
- mCallback.onConnectionFailed();
- }
-
- if (DEBUG) {
- Log.d(TAG, "connect...");
- dump();
- }
- }
- });
- }
-
- @Override
- public void disconnect() {
- // It's ok to call this any state, because allowing this lets apps not have
- // to check isConnected() unnecessarily. They won't appreciate the extra
- // assertions for this. We do everything we can here to go back to a sane state.
- mState = CONNECT_STATE_DISCONNECTING;
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // connect() could be called before this. Then we will disconnect and reconnect.
- if (mCallbacksMessenger != null) {
- try {
- mServiceBinderWrapper.disconnect(mCallbacksMessenger);
- } catch (RemoteException ex) {
- // We are disconnecting anyway. Log, just for posterity but it's not
- // a big problem.
- Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
- }
- }
- int state = mState;
- forceCloseConnection();
- // If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that
- // the operation came after disconnect() can be handled properly.
- if (state != CONNECT_STATE_DISCONNECTING) {
- mState = state;
- }
- if (DEBUG) {
- Log.d(TAG, "disconnect...");
- dump();
- }
- }
- });
- }
-
- /**
- * Null out the variables and unbind from the service. This doesn't include
- * calling disconnect on the service, because we only try to do that in the
- * clean shutdown cases.
- * <p>
- * Everywhere that calls this EXCEPT for disconnect() should follow it with
- * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
- * for a clean shutdown, but everywhere else is a dirty shutdown and should
- * notify the app.
- */
- void forceCloseConnection() {
- if (mServiceConnection != null) {
- mContext.unbindService(mServiceConnection);
- }
- mState = CONNECT_STATE_DISCONNECTED;
- mServiceConnection = null;
- mServiceBinderWrapper = null;
- mCallbacksMessenger = null;
- mHandler.setCallbacksMessenger(null);
- mRootId = null;
- mMediaSessionToken = null;
- }
-
- @Override
- public boolean isConnected() {
- return mState == CONNECT_STATE_CONNECTED;
- }
-
- @Override
- public @NonNull ComponentName getServiceComponent() {
- if (!isConnected()) {
- throw new IllegalStateException("getServiceComponent() called while not connected" +
- " (state=" + mState + ")");
- }
- return mServiceComponent;
- }
-
- @Override
- public @NonNull String getRoot() {
- if (!isConnected()) {
- throw new IllegalStateException("getRoot() called while not connected"
- + "(state=" + getStateLabel(mState) + ")");
- }
- return mRootId;
- }
-
- @Override
- public @Nullable Bundle getExtras() {
- if (!isConnected()) {
- throw new IllegalStateException("getExtras() called while not connected (state="
- + getStateLabel(mState) + ")");
- }
- return mExtras;
- }
-
- @Override
- public @NonNull MediaSessionCompat.Token getSessionToken() {
- if (!isConnected()) {
- throw new IllegalStateException("getSessionToken() called while not connected"
- + "(state=" + mState + ")");
- }
- return mMediaSessionToken;
- }
-
- @Override
- public void subscribe(@NonNull String parentId, Bundle options,
- @NonNull SubscriptionCallback callback) {
- // Update or create the subscription.
- Subscription sub = mSubscriptions.get(parentId);
- if (sub == null) {
- sub = new Subscription();
- mSubscriptions.put(parentId, sub);
- }
- Bundle copiedOptions = options == null ? null : new Bundle(options);
- sub.putCallback(mContext, copiedOptions, callback);
-
- // If we are connected, tell the service that we are watching. If we aren't
- // connected, the service will be told when we connect.
- if (isConnected()) {
- try {
- mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
- mCallbacksMessenger);
- } catch (RemoteException e) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
- }
- }
- }
-
- @Override
- public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
- Subscription sub = mSubscriptions.get(parentId);
- if (sub == null) {
- return;
- }
-
- // Tell the service if necessary.
- try {
- if (callback == null) {
- if (isConnected()) {
- mServiceBinderWrapper.removeSubscription(parentId, null,
- mCallbacksMessenger);
- }
- } else {
- final List<SubscriptionCallback> callbacks = sub.getCallbacks();
- final List<Bundle> optionsList = sub.getOptionsList();
- for (int i = callbacks.size() - 1; i >= 0; --i) {
- if (callbacks.get(i) == callback) {
- if (isConnected()) {
- mServiceBinderWrapper.removeSubscription(
- parentId, callback.mToken, mCallbacksMessenger);
- }
- callbacks.remove(i);
- optionsList.remove(i);
- }
- }
- }
- } catch (RemoteException ex) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
- }
-
- if (sub.isEmpty() || callback == null) {
- mSubscriptions.remove(parentId);
- }
- }
-
- @Override
- public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
- if (TextUtils.isEmpty(mediaId)) {
- throw new IllegalArgumentException("mediaId is empty");
- }
- if (cb == null) {
- throw new IllegalArgumentException("cb is null");
- }
- if (!isConnected()) {
- Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- cb.onError(mediaId);
- }
- });
- return;
- }
- ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
- try {
- mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error getting media item: " + mediaId);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- cb.onError(mediaId);
- }
- });
- }
- }
-
- @Override
- public void search(@NonNull final String query, final Bundle extras,
- @NonNull final SearchCallback callback) {
- if (!isConnected()) {
- throw new IllegalStateException("search() called while not connected"
- + " (state=" + getStateLabel(mState) + ")");
- }
-
- ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
- try {
- mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error searching items with query: " + query, e);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onError(query, extras);
- }
- });
- }
- }
-
- @Override
- public void sendCustomAction(@NonNull final String action, final Bundle extras,
- @Nullable final CustomActionCallback callback) {
- if (!isConnected()) {
- throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
- + "extras " + extras + " because the browser is not connected to the "
- + "service.");
- }
-
- ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
- mHandler);
- try {
- mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
- mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
- + extras, e);
- if (callback != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onError(action, extras, null);
- }
- });
- }
- }
- }
-
- @Override
- public void onServiceConnected(final Messenger callback, final String root,
- final MediaSessionCompat.Token session, final Bundle extra) {
- // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
- if (!isCurrent(callback, "onConnect")) {
- return;
- }
- // Don't allow them to call us twice.
- if (mState != CONNECT_STATE_CONNECTING) {
- Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState)
- + "... ignoring");
- return;
- }
- mRootId = root;
- mMediaSessionToken = session;
- mExtras = extra;
- mState = CONNECT_STATE_CONNECTED;
-
- if (DEBUG) {
- Log.d(TAG, "ServiceCallbacks.onConnect...");
- dump();
- }
- mCallback.onConnected();
-
- // we may receive some subscriptions before we are connected, so re-subscribe
- // everything now
- try {
- for (Map.Entry<String, Subscription> subscriptionEntry
- : mSubscriptions.entrySet()) {
- String id = subscriptionEntry.getKey();
- Subscription sub = subscriptionEntry.getValue();
- List<SubscriptionCallback> callbackList = sub.getCallbacks();
- List<Bundle> optionsList = sub.getOptionsList();
- for (int i = 0; i < callbackList.size(); ++i) {
- mServiceBinderWrapper.addSubscription(id, callbackList.get(i).mToken,
- optionsList.get(i), mCallbacksMessenger);
- }
- }
- } catch (RemoteException ex) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "addSubscription failed with RemoteException.");
- }
- }
-
- @Override
- public void onConnectionFailed(final Messenger callback) {
- Log.e(TAG, "onConnectFailed for " + mServiceComponent);
-
- // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
- if (!isCurrent(callback, "onConnectFailed")) {
- return;
- }
- // Don't allow them to call us twice.
- if (mState != CONNECT_STATE_CONNECTING) {
- Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState)
- + "... ignoring");
- return;
- }
-
- // Clean up
- forceCloseConnection();
-
- // Tell the app.
- mCallback.onConnectionFailed();
- }
-
- @Override
- public void onLoadChildren(final Messenger callback, final String parentId,
- final List list, final Bundle options) {
- // Check that there hasn't been a disconnect or a different ServiceConnection.
- if (!isCurrent(callback, "onLoadChildren")) {
- return;
- }
-
- if (DEBUG) {
- Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
- }
-
- // Check that the subscription is still subscribed.
- final Subscription subscription = mSubscriptions.get(parentId);
- if (subscription == null) {
- if (DEBUG) {
- Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
- }
- return;
- }
-
- // Tell the app.
- SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
- if (subscriptionCallback != null) {
- if (options == null) {
- if (list == null) {
- subscriptionCallback.onError(parentId);
- } else {
- subscriptionCallback.onChildrenLoaded(parentId, list);
- }
- } else {
- if (list == null) {
- subscriptionCallback.onError(parentId, options);
- } else {
- subscriptionCallback.onChildrenLoaded(parentId, list, options);
- }
- }
- }
- }
-
- /**
- * For debugging.
- */
- private static String getStateLabel(int state) {
- switch (state) {
- case CONNECT_STATE_DISCONNECTING:
- return "CONNECT_STATE_DISCONNECTING";
- case CONNECT_STATE_DISCONNECTED:
- return "CONNECT_STATE_DISCONNECTED";
- case CONNECT_STATE_CONNECTING:
- return "CONNECT_STATE_CONNECTING";
- case CONNECT_STATE_CONNECTED:
- return "CONNECT_STATE_CONNECTED";
- case CONNECT_STATE_SUSPENDED:
- return "CONNECT_STATE_SUSPENDED";
- default:
- return "UNKNOWN/" + state;
- }
- }
-
- /**
- * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
- */
- @SuppressWarnings("ReferenceEquality")
- private boolean isCurrent(Messenger callback, String funcName) {
- if (mCallbacksMessenger != callback || mState == CONNECT_STATE_DISCONNECTING
- || mState == CONNECT_STATE_DISCONNECTED) {
- if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
- Log.i(TAG, funcName + " for " + mServiceComponent + " with mCallbacksMessenger="
- + mCallbacksMessenger + " this=" + this);
- }
- return false;
- }
- return true;
- }
-
- /**
- * Log internal state.
- */
- void dump() {
- Log.d(TAG, "MediaBrowserCompat...");
- Log.d(TAG, " mServiceComponent=" + mServiceComponent);
- Log.d(TAG, " mCallback=" + mCallback);
- Log.d(TAG, " mRootHints=" + mRootHints);
- Log.d(TAG, " mState=" + getStateLabel(mState));
- Log.d(TAG, " mServiceConnection=" + mServiceConnection);
- Log.d(TAG, " mServiceBinderWrapper=" + mServiceBinderWrapper);
- Log.d(TAG, " mCallbacksMessenger=" + mCallbacksMessenger);
- Log.d(TAG, " mRootId=" + mRootId);
- Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
- }
-
- /**
- * ServiceConnection to the other app.
- */
- private class MediaServiceConnection implements ServiceConnection {
- MediaServiceConnection() {
- }
-
- @Override
- public void onServiceConnected(final ComponentName name, final IBinder binder) {
- postOrRun(new Runnable() {
- @Override
- public void run() {
- if (DEBUG) {
- Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
- + " binder=" + binder);
- dump();
- }
-
- // Make sure we are still the current connection, and that they haven't
- // called disconnect().
- if (!isCurrent("onServiceConnected")) {
- return;
- }
-
- // Save their binder
- mServiceBinderWrapper = new ServiceBinderWrapper(binder, mRootHints);
-
- // We make a new mServiceCallbacks each time we connect so that we can drop
- // responses from previous connections.
- mCallbacksMessenger = new Messenger(mHandler);
- mHandler.setCallbacksMessenger(mCallbacksMessenger);
-
- mState = CONNECT_STATE_CONNECTING;
-
- // Call connect, which is async. When we get a response from that we will
- // say that we're connected.
- try {
- if (DEBUG) {
- Log.d(TAG, "ServiceCallbacks.onConnect...");
- dump();
- }
- mServiceBinderWrapper.connect(mContext, mCallbacksMessenger);
- } catch (RemoteException ex) {
- // Connect failed, which isn't good. But the auto-reconnect on the
- // service will take over and we will come back. We will also get the
- // onServiceDisconnected, which has all the cleanup code. So let that
- // do it.
- Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
- if (DEBUG) {
- Log.d(TAG, "ServiceCallbacks.onConnect...");
- dump();
- }
- }
- }
- });
- }
-
- @Override
- public void onServiceDisconnected(final ComponentName name) {
- postOrRun(new Runnable() {
- @Override
- public void run() {
- if (DEBUG) {
- Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
- + " this=" + this + " mServiceConnection=" +
- mServiceConnection);
- dump();
- }
-
- // Make sure we are still the current connection, and that they haven't
- // called disconnect().
- if (!isCurrent("onServiceDisconnected")) {
- return;
- }
-
- // Clear out what we set in onServiceConnected
- mServiceBinderWrapper = null;
- mCallbacksMessenger = null;
- mHandler.setCallbacksMessenger(null);
-
- // And tell the app that it's suspended.
- mState = CONNECT_STATE_SUSPENDED;
- mCallback.onConnectionSuspended();
- }
- });
- }
-
- private void postOrRun(Runnable r) {
- if (Thread.currentThread() == mHandler.getLooper().getThread()) {
- r.run();
- } else {
- mHandler.post(r);
- }
- }
-
- /**
- * Return true if this is the current ServiceConnection. Also logs if it's not.
- */
- boolean isCurrent(String funcName) {
- if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING
- || mState == CONNECT_STATE_DISCONNECTED) {
- if (mState != CONNECT_STATE_DISCONNECTING
- && mState != CONNECT_STATE_DISCONNECTED) {
- // Check mState, because otherwise this log is noisy.
- Log.i(TAG, funcName + " for " + mServiceComponent +
- " with mServiceConnection=" + mServiceConnection + " this=" + this);
- }
- return false;
- }
- return true;
- }
- }
- }
-
- @RequiresApi(21)
- static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl,
- ConnectionCallback.ConnectionCallbackInternal {
- final Context mContext;
- protected final Object mBrowserObj;
- protected final Bundle mRootHints;
- protected final CallbackHandler mHandler = new CallbackHandler(this);
- private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
-
- protected int mServiceVersion;
- protected ServiceBinderWrapper mServiceBinderWrapper;
- protected Messenger mCallbacksMessenger;
- private MediaSessionCompat.Token mMediaSessionToken;
-
- MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
- ConnectionCallback callback, Bundle rootHints) {
- mContext = context;
- if (rootHints == null) {
- rootHints = new Bundle();
- }
- rootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT);
- mRootHints = new Bundle(rootHints);
- callback.setInternalConnectionCallback(this);
- mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent,
- callback.mConnectionCallbackObj, mRootHints);
- }
-
- @Override
- public void connect() {
- MediaBrowserCompatApi21.connect(mBrowserObj);
- }
-
- @Override
- public void disconnect() {
- if (mServiceBinderWrapper != null && mCallbacksMessenger != null) {
- try {
- mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error unregistering client messenger." );
- }
- }
- MediaBrowserCompatApi21.disconnect(mBrowserObj);
- }
-
- @Override
- public boolean isConnected() {
- return MediaBrowserCompatApi21.isConnected(mBrowserObj);
- }
-
- @Override
- public ComponentName getServiceComponent() {
- return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj);
- }
-
- @NonNull
- @Override
- public String getRoot() {
- return MediaBrowserCompatApi21.getRoot(mBrowserObj);
- }
-
- @Nullable
- @Override
- public Bundle getExtras() {
- return MediaBrowserCompatApi21.getExtras(mBrowserObj);
- }
-
- @NonNull
- @Override
- public MediaSessionCompat.Token getSessionToken() {
- if (mMediaSessionToken == null) {
- mMediaSessionToken = MediaSessionCompat.Token.fromToken(
- MediaBrowserCompatApi21.getSessionToken(mBrowserObj));
- }
- return mMediaSessionToken;
- }
-
- @Override
- public void subscribe(@NonNull final String parentId, final Bundle options,
- @NonNull final SubscriptionCallback callback) {
- // Update or create the subscription.
- Subscription sub = mSubscriptions.get(parentId);
- if (sub == null) {
- sub = new Subscription();
- mSubscriptions.put(parentId, sub);
- }
- callback.setSubscription(sub);
- Bundle copiedOptions = options == null ? null : new Bundle(options);
- sub.putCallback(mContext, copiedOptions, callback);
-
- if (mServiceBinderWrapper == null) {
- // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
- // subscribe with options won't work properly.
- MediaBrowserCompatApi21.subscribe(
- mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
- } else {
- try {
- mServiceBinderWrapper.addSubscription(
- parentId, callback.mToken, copiedOptions, mCallbacksMessenger);
- } catch (RemoteException e) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.i(TAG, "Remote error subscribing media item: " + parentId);
- }
- }
- }
-
- @Override
- public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
- Subscription sub = mSubscriptions.get(parentId);
- if (sub == null) {
- return;
- }
-
- if (mServiceBinderWrapper == null) {
- if (callback == null) {
- MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
- } else {
- final List<SubscriptionCallback> callbacks = sub.getCallbacks();
- final List<Bundle> optionsList = sub.getOptionsList();
- for (int i = callbacks.size() - 1; i >= 0; --i) {
- if (callbacks.get(i) == callback) {
- callbacks.remove(i);
- optionsList.remove(i);
- }
- }
- if (callbacks.size() == 0) {
- MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
- }
- }
- } else {
- // Tell the service if necessary.
- try {
- if (callback == null) {
- mServiceBinderWrapper.removeSubscription(parentId, null,
- mCallbacksMessenger);
- } else {
- final List<SubscriptionCallback> callbacks = sub.getCallbacks();
- final List<Bundle> optionsList = sub.getOptionsList();
- for (int i = callbacks.size() - 1; i >= 0; --i) {
- if (callbacks.get(i) == callback) {
- mServiceBinderWrapper.removeSubscription(
- parentId, callback.mToken, mCallbacksMessenger);
- callbacks.remove(i);
- optionsList.remove(i);
- }
- }
- }
- } catch (RemoteException ex) {
- // Process is crashing. We will disconnect, and upon reconnect we will
- // automatically reregister. So nothing to do here.
- Log.d(TAG, "removeSubscription failed with RemoteException parentId="
- + parentId);
- }
- }
-
- if (sub.isEmpty() || callback == null) {
- mSubscriptions.remove(parentId);
- }
- }
-
- @Override
- public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
- if (TextUtils.isEmpty(mediaId)) {
- throw new IllegalArgumentException("mediaId is empty");
- }
- if (cb == null) {
- throw new IllegalArgumentException("cb is null");
- }
- if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) {
- Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- cb.onError(mediaId);
- }
- });
- return;
- }
- if (mServiceBinderWrapper == null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // Default framework implementation.
- cb.onError(mediaId);
- }
- });
- return;
- }
- ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
- try {
- mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error getting media item: " + mediaId);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- cb.onError(mediaId);
- }
- });
- }
- }
-
- @Override
- public void search(@NonNull final String query, final Bundle extras,
- @NonNull final SearchCallback callback) {
- if (!isConnected()) {
- throw new IllegalStateException("search() called while not connected");
- }
- if (mServiceBinderWrapper == null) {
- Log.i(TAG, "The connected service doesn't support search.");
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // Default framework implementation.
- callback.onError(query, extras);
- }
- });
- return;
- }
-
- ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
- try {
- mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error searching items with query: " + query, e);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onError(query, extras);
- }
- });
- }
- }
-
- @Override
- public void sendCustomAction(@NonNull final String action, final Bundle extras,
- @Nullable final CustomActionCallback callback) {
- if (!isConnected()) {
- throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
- + "extras " + extras + " because the browser is not connected to the "
- + "service.");
- }
- if (mServiceBinderWrapper == null) {
- Log.i(TAG, "The connected service doesn't support sendCustomAction.");
- if (callback != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onError(action, extras, null);
- }
- });
- }
- }
-
- ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
- mHandler);
- try {
- mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
- mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
- + extras, e);
- if (callback != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onError(action, extras, null);
- }
- });
- }
- }
- }
-
- @Override
- public void onConnected() {
- Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
- if (extras == null) {
- return;
- }
- mServiceVersion = extras.getInt(EXTRA_SERVICE_VERSION, 0);
- IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER);
- if (serviceBinder != null) {
- mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints);
- mCallbacksMessenger = new Messenger(mHandler);
- mHandler.setCallbacksMessenger(mCallbacksMessenger);
- try {
- mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger);
- } catch (RemoteException e) {
- Log.i(TAG, "Remote error registering client messenger." );
- }
- }
- IMediaSession sessionToken = IMediaSession.Stub.asInterface(
- BundleCompat.getBinder(extras, EXTRA_SESSION_BINDER));
- if (sessionToken != null) {
- mMediaSessionToken = MediaSessionCompat.Token.fromToken(
- MediaBrowserCompatApi21.getSessionToken(mBrowserObj), sessionToken);
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- mServiceBinderWrapper = null;
- mCallbacksMessenger = null;
- mMediaSessionToken = null;
- mHandler.setCallbacksMessenger(null);
- }
-
- @Override
- public void onConnectionFailed() {
- // Do noting
- }
-
- @Override
- public void onServiceConnected(final Messenger callback, final String root,
- final MediaSessionCompat.Token session, final Bundle extra) {
- // This method will not be called.
- }
-
- @Override
- public void onConnectionFailed(Messenger callback) {
- // This method will not be called.
- }
-
- @Override
- @SuppressWarnings("ReferenceEquality")
- public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) {
- if (mCallbacksMessenger != callback) {
- return;
- }
-
- // Check that the subscription is still subscribed.
- Subscription subscription = mSubscriptions.get(parentId);
- if (subscription == null) {
- if (DEBUG) {
- Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
- }
- return;
- }
-
- // Tell the app.
- SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
- if (subscriptionCallback != null) {
- if (options == null) {
- if (list == null) {
- subscriptionCallback.onError(parentId);
- } else {
- subscriptionCallback.onChildrenLoaded(parentId, list);
- }
- } else {
- if (list == null) {
- subscriptionCallback.onError(parentId, options);
- } else {
- subscriptionCallback.onChildrenLoaded(parentId, list, options);
- }
- }
- }
- }
- }
-
- @RequiresApi(23)
- static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 {
- MediaBrowserImplApi23(Context context, ComponentName serviceComponent,
- ConnectionCallback callback, Bundle rootHints) {
- super(context, serviceComponent, callback, rootHints);
- }
-
- @Override
- public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
- if (mServiceBinderWrapper == null) {
- MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj);
- } else {
- super.getItem(mediaId, cb);
- }
- }
- }
-
- @RequiresApi(26)
- static class MediaBrowserImplApi26 extends MediaBrowserImplApi23 {
- MediaBrowserImplApi26(Context context, ComponentName serviceComponent,
- ConnectionCallback callback, Bundle rootHints) {
- super(context, serviceComponent, callback, rootHints);
- }
-
- @Override
- public void subscribe(@NonNull String parentId, @Nullable Bundle options,
- @NonNull SubscriptionCallback callback) {
- // From service v2, we use compat code when subscribing.
- // This is to prevent ClassNotFoundException when options has Parcelable in it.
- if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
- if (options == null) {
- MediaBrowserCompatApi21.subscribe(
- mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
- } else {
- MediaBrowserCompatApi26.subscribe(
- mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj);
- }
- } else {
- super.subscribe(parentId, options, callback);
- }
- }
-
- @Override
- public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
- // From service v2, we use compat code when subscribing.
- // This is to prevent ClassNotFoundException when options has Parcelable in it.
- if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
- if (callback == null) {
- MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
- } else {
- MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId,
- callback.mSubscriptionCallbackObj);
- }
- } else {
- super.unsubscribe(parentId, callback);
- }
- }
- }
-
- private static class Subscription {
- private final List<SubscriptionCallback> mCallbacks;
- private final List<Bundle> mOptionsList;
-
- public Subscription() {
- mCallbacks = new ArrayList<>();
- mOptionsList = new ArrayList<>();
- }
-
- public boolean isEmpty() {
- return mCallbacks.isEmpty();
- }
-
- public List<Bundle> getOptionsList() {
- return mOptionsList;
- }
-
- public List<SubscriptionCallback> getCallbacks() {
- return mCallbacks;
- }
-
- public SubscriptionCallback getCallback(Context context, Bundle options) {
- if (options != null) {
- options.setClassLoader(context.getClassLoader());
- }
- for (int i = 0; i < mOptionsList.size(); ++i) {
- if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
- return mCallbacks.get(i);
- }
- }
- return null;
- }
-
- public void putCallback(Context context, Bundle options, SubscriptionCallback callback) {
- if (options != null) {
- options.setClassLoader(context.getClassLoader());
- }
- for (int i = 0; i < mOptionsList.size(); ++i) {
- if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
- mCallbacks.set(i, callback);
- return;
- }
- }
- mCallbacks.add(callback);
- mOptionsList.add(options);
- }
- }
-
- private static class CallbackHandler extends Handler {
- private final WeakReference<MediaBrowserServiceCallbackImpl> mCallbackImplRef;
- private WeakReference<Messenger> mCallbacksMessengerRef;
-
- CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) {
- super();
- mCallbackImplRef = new WeakReference<>(callbackImpl);
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (mCallbacksMessengerRef == null || mCallbacksMessengerRef.get() == null ||
- mCallbackImplRef.get() == null) {
- return;
- }
- Bundle data = msg.getData();
- data.setClassLoader(MediaSessionCompat.class.getClassLoader());
- MediaBrowserServiceCallbackImpl serviceCallback = mCallbackImplRef.get();
- Messenger callbacksMessenger = mCallbacksMessengerRef.get();
- try {
- switch (msg.what) {
- case SERVICE_MSG_ON_CONNECT:
- serviceCallback.onServiceConnected(callbacksMessenger,
- data.getString(DATA_MEDIA_ITEM_ID),
- (MediaSessionCompat.Token) data.getParcelable(
- DATA_MEDIA_SESSION_TOKEN),
- data.getBundle(DATA_ROOT_HINTS));
- break;
- case SERVICE_MSG_ON_CONNECT_FAILED:
- serviceCallback.onConnectionFailed(callbacksMessenger);
- break;
- case SERVICE_MSG_ON_LOAD_CHILDREN:
- serviceCallback.onLoadChildren(callbacksMessenger,
- data.getString(DATA_MEDIA_ITEM_ID),
- data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST),
- data.getBundle(DATA_OPTIONS));
- break;
- default:
- Log.w(TAG, "Unhandled message: " + msg
- + "\n Client version: " + CLIENT_VERSION_CURRENT
- + "\n Service version: " + msg.arg1);
- }
- } catch (BadParcelableException e) {
- // Do not print the exception here, since it is already done by the Parcel class.
- Log.e(TAG, "Could not unparcel the data.");
- // If an error happened while connecting, disconnect from the service.
- if (msg.what == SERVICE_MSG_ON_CONNECT) {
- serviceCallback.onConnectionFailed(callbacksMessenger);
- }
- }
- }
-
- void setCallbacksMessenger(Messenger callbacksMessenger) {
- mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger);
- }
- }
-
- private static class ServiceBinderWrapper {
- private Messenger mMessenger;
- private Bundle mRootHints;
-
- public ServiceBinderWrapper(IBinder target, Bundle rootHints) {
- mMessenger = new Messenger(target);
- mRootHints = rootHints;
- }
-
- void connect(Context context, Messenger callbacksMessenger)
- throws RemoteException {
- Bundle data = new Bundle();
- data.putString(DATA_PACKAGE_NAME, context.getPackageName());
- data.putBundle(DATA_ROOT_HINTS, mRootHints);
- sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger);
- }
-
- void disconnect(Messenger callbacksMessenger) throws RemoteException {
- sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger);
- }
-
- void addSubscription(String parentId, IBinder callbackToken, Bundle options,
- Messenger callbacksMessenger)
- throws RemoteException {
- Bundle data = new Bundle();
- data.putString(DATA_MEDIA_ITEM_ID, parentId);
- BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
- data.putBundle(DATA_OPTIONS, options);
- sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger);
- }
-
- void removeSubscription(String parentId, IBinder callbackToken,
- Messenger callbacksMessenger)
- throws RemoteException {
- Bundle data = new Bundle();
- data.putString(DATA_MEDIA_ITEM_ID, parentId);
- BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
- sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger);
- }
-
- void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger)
- throws RemoteException {
- Bundle data = new Bundle();
- data.putString(DATA_MEDIA_ITEM_ID, mediaId);
- data.putParcelable(DATA_RESULT_RECEIVER, receiver);
- sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger);
- }
-
- void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
- Bundle data = new Bundle();
- data.putBundle(DATA_ROOT_HINTS, mRootHints);
- sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger);
- }
-
- void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
- sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
- }
-
- void search(String query, Bundle extras, ResultReceiver receiver,
- Messenger callbacksMessenger) throws RemoteException {
- Bundle data = new Bundle();
- data.putString(DATA_SEARCH_QUERY, query);
- data.putBundle(DATA_SEARCH_EXTRAS, extras);
- data.putParcelable(DATA_RESULT_RECEIVER, receiver);
- sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
- }
-
- void sendCustomAction(String action, Bundle extras, ResultReceiver receiver,
- Messenger callbacksMessenger) throws RemoteException {
- Bundle data = new Bundle();
- data.putString(DATA_CUSTOM_ACTION, action);
- data.putBundle(DATA_CUSTOM_ACTION_EXTRAS, extras);
- data.putParcelable(DATA_RESULT_RECEIVER, receiver);
- sendRequest(CLIENT_MSG_SEND_CUSTOM_ACTION, data, callbacksMessenger);
- }
-
- private void sendRequest(int what, Bundle data, Messenger cbMessenger)
- throws RemoteException {
- Message msg = Message.obtain();
- msg.what = what;
- msg.arg1 = CLIENT_VERSION_CURRENT;
- msg.setData(data);
- msg.replyTo = cbMessenger;
- mMessenger.send(msg);
- }
- }
-
- private static class ItemReceiver extends ResultReceiver {
- private final String mMediaId;
- private final ItemCallback mCallback;
-
- ItemReceiver(String mediaId, ItemCallback callback, Handler handler) {
- super(handler);
- mMediaId = mediaId;
- mCallback = callback;
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (resultData != null) {
- resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
- }
- if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
- || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
- mCallback.onError(mMediaId);
- return;
- }
- Parcelable item = resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
- if (item == null || item instanceof MediaItem) {
- mCallback.onItemLoaded((MediaItem) item);
- } else {
- mCallback.onError(mMediaId);
- }
- }
- }
-
- private static class SearchResultReceiver extends ResultReceiver {
- private final String mQuery;
- private final Bundle mExtras;
- private final SearchCallback mCallback;
-
- SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
- Handler handler) {
- super(handler);
- mQuery = query;
- mExtras = extras;
- mCallback = callback;
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (resultData != null) {
- resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
- }
- if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
- || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
- mCallback.onError(mQuery, mExtras);
- return;
- }
- Parcelable[] items = resultData.getParcelableArray(
- MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
- List<MediaItem> results = null;
- if (items != null) {
- results = new ArrayList<>();
- for (Parcelable item : items) {
- results.add((MediaItem) item);
- }
- }
- mCallback.onSearchResult(mQuery, mExtras, results);
- }
- }
-
- private static class CustomActionResultReceiver extends ResultReceiver {
- private final String mAction;
- private final Bundle mExtras;
- private final CustomActionCallback mCallback;
-
- CustomActionResultReceiver(String action, Bundle extras, CustomActionCallback callback,
- Handler handler) {
- super(handler);
- mAction = action;
- mExtras = extras;
- mCallback = callback;
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (mCallback == null) {
- return;
- }
- switch (resultCode) {
- case MediaBrowserServiceCompat.RESULT_PROGRESS_UPDATE:
- mCallback.onProgressUpdate(mAction, mExtras, resultData);
- break;
- case MediaBrowserServiceCompat.RESULT_OK:
- mCallback.onResult(mAction, mExtras, resultData);
- break;
- case MediaBrowserServiceCompat.RESULT_ERROR:
- mCallback.onError(mAction, mExtras, resultData);
- break;
- default:
- Log.w(TAG, "Unknown result code: " + resultCode + " (extras=" + mExtras
- + ", resultData=" + resultData + ")");
- break;
- }
- }
- }
-}
diff --git a/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java b/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
index dba32ca..d3f8bbb 100644
--- a/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
+++ b/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
@@ -19,6 +19,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
import androidx.annotation.RestrictTo;
diff --git a/media/src/main/java/androidx/media/MediaBrowserProtocol.java b/media/src/main/java/androidx/media/MediaBrowserProtocol.java
index 2449435..251158f 100644
--- a/media/src/main/java/androidx/media/MediaBrowserProtocol.java
+++ b/media/src/main/java/androidx/media/MediaBrowserProtocol.java
@@ -15,10 +15,17 @@
*/
package androidx.media;
+import android.support.v4.media.MediaBrowserCompat;
+
+import androidx.annotation.RestrictTo;
+
/**
* Defines the communication protocol for media browsers and media browser services.
+ *
+ * @hide
*/
-class MediaBrowserProtocol {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MediaBrowserProtocol {
public static final String DATA_CALLBACK_TOKEN = "data_callback_token";
public static final String DATA_CALLING_UID = "data_calling_uid";
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index 3b89fd3..8f18cfc 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -16,7 +16,7 @@
package androidx.media;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
@@ -60,6 +60,7 @@
import android.os.Messenger;
import android.os.Parcel;
import android.os.RemoteException;
+import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.IMediaSession;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.os.ResultReceiver;
@@ -130,7 +131,7 @@
*
* @hide
*/
- @RestrictTo(LIBRARY_GROUP)
+ @RestrictTo(LIBRARY)
public static final String KEY_MEDIA_ITEM = "media_item";
/**
@@ -138,19 +139,32 @@
*
* @hide
*/
- @RestrictTo(LIBRARY_GROUP)
+ @RestrictTo(LIBRARY)
public static final String KEY_SEARCH_RESULTS = "search_results";
static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2;
- static final int RESULT_ERROR = -1;
- static final int RESULT_OK = 0;
- static final int RESULT_PROGRESS_UPDATE = 1;
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final int RESULT_ERROR = -1;
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final int RESULT_OK = 0;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final int RESULT_PROGRESS_UPDATE = 1;
/** @hide */
- @RestrictTo(LIBRARY_GROUP)
+ @RestrictTo(LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
diff --git a/media/src/main/java/androidx/media/session/MediaButtonReceiver.java b/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
index 113cdfd..15b9e95 100644
--- a/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
+++ b/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
@@ -28,6 +28,8 @@
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.RemoteException;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.session.PlaybackStateCompat.MediaKeyAction;
@@ -35,7 +37,6 @@
import android.view.KeyEvent;
import androidx.annotation.RestrictTo;
-import androidx.media.MediaBrowserCompat;
import androidx.media.MediaBrowserServiceCompat;
import java.util.List;
diff --git a/media/src/main/java/androidx/media/session/MediaControllerCompat.java b/media/src/main/java/androidx/media/session/MediaControllerCompat.java
deleted file mode 100644
index 5fcf429..0000000
--- a/media/src/main/java/androidx/media/session/MediaControllerCompat.java
+++ /dev/null
@@ -1,2494 +0,0 @@
-/*
- * Copyright 2018 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 androidx.media.session;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.media.AudioManager;
-import android.media.session.MediaController;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.RemoteException;
-import android.os.ResultReceiver;
-import android.support.v4.media.MediaDescriptionCompat;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.RatingCompat;
-import android.support.v4.media.session.IMediaControllerCallback;
-import android.support.v4.media.session.IMediaSession;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.MediaSessionCompat.QueueItem;
-import android.support.v4.media.session.ParcelableVolumeInfo;
-import android.support.v4.media.session.PlaybackStateCompat;
-import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.KeyEvent;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.core.app.BundleCompat;
-import androidx.core.app.SupportActivity;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.VolumeProviderCompat;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-
-/**
- * Allows an app to interact with an ongoing media session. Media buttons and
- * other commands can be sent to the session. A callback may be registered to
- * receive updates from the session, such as metadata and play state changes.
- * <p>
- * A MediaController can be created if you have a {@link MediaSessionCompat.Token}
- * from the session owner.
- * <p>
- * MediaController objects are thread-safe.
- * <p>
- * This is a helper for accessing features in {@link android.media.session.MediaSession}
- * introduced after API level 4 in a backwards compatible fashion.
- * <p class="note">
- * If MediaControllerCompat is created with a {@link MediaSessionCompat.Token session token}
- * from another process, following methods will not work directly after the creation if the
- * {@link MediaSessionCompat.Token session token} is not passed through a
- * {@link MediaBrowserCompat}:
- * <ul>
- * <li>{@link #getPlaybackState()}.{@link PlaybackStateCompat#getExtras() getExtras()}</li>
- * <li>{@link #isCaptioningEnabled()}</li>
- * <li>{@link #getRepeatMode()}</li>
- * <li>{@link #getShuffleMode()}</li>
- * </ul></p>
- *
- * <div class="special reference">
- * <h3>Developer Guides</h3>
- * <p>For information about building your media application, read the
- * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
- * </div>
- */
-public final class MediaControllerCompat {
- static final String TAG = "MediaControllerCompat";
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_GET_EXTRA_BINDER =
- "android.support.v4.media.session.command.GET_EXTRA_BINDER";
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_ADD_QUEUE_ITEM =
- "android.support.v4.media.session.command.ADD_QUEUE_ITEM";
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_ADD_QUEUE_ITEM_AT =
- "android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT";
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_REMOVE_QUEUE_ITEM =
- "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM";
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_REMOVE_QUEUE_ITEM_AT =
- "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT";
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION =
- "android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION";
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final String COMMAND_ARGUMENT_INDEX =
- "android.support.v4.media.session.command.ARGUMENT_INDEX";
-
- private static class MediaControllerExtraData extends SupportActivity.ExtraData {
- private final MediaControllerCompat mMediaController;
-
- MediaControllerExtraData(MediaControllerCompat mediaController) {
- mMediaController = mediaController;
- }
-
- MediaControllerCompat getMediaController() {
- return mMediaController;
- }
- }
-
- /**
- * Sets a {@link MediaControllerCompat} in the {@code activity} for later retrieval via
- * {@link #getMediaController(Activity)}.
- *
- * <p>This is compatible with {@link Activity#setMediaController(MediaController)}.
- * If {@code activity} inherits {@link androidx.fragment.app.FragmentActivity}, the
- * {@code mediaController} will be saved in the {@code activity}. In addition to that,
- * on API 21 and later, {@link Activity#setMediaController(MediaController)} will be
- * called.</p>
- *
- * @param activity The activity to set the {@code mediaController} in, must not be null.
- * @param mediaController The controller for the session which should receive
- * media keys and volume changes on API 21 and later.
- * @see #getMediaController(Activity)
- * @see Activity#setMediaController(android.media.session.MediaController)
- */
- public static void setMediaController(@NonNull Activity activity,
- MediaControllerCompat mediaController) {
- if (activity instanceof SupportActivity) {
- ((SupportActivity) activity).putExtraData(
- new MediaControllerExtraData(mediaController));
- }
- if (android.os.Build.VERSION.SDK_INT >= 21) {
- Object controllerObj = null;
- if (mediaController != null) {
- Object sessionTokenObj = mediaController.getSessionToken().getToken();
- controllerObj = MediaControllerCompatApi21.fromToken(activity, sessionTokenObj);
- }
- MediaControllerCompatApi21.setMediaController(activity, controllerObj);
- }
- }
-
- /**
- * Retrieves the {@link MediaControllerCompat} set in the activity by
- * {@link #setMediaController(Activity, MediaControllerCompat)} for sending media key and volume
- * events.
- *
- * <p>This is compatible with {@link Activity#getMediaController()}.</p>
- *
- * @param activity The activity to get the media controller from, must not be null.
- * @return The controller which should receive events.
- * @see #setMediaController(Activity, MediaControllerCompat)
- */
- public static MediaControllerCompat getMediaController(@NonNull Activity activity) {
- if (activity instanceof SupportActivity) {
- MediaControllerExtraData extraData =
- ((SupportActivity) activity).getExtraData(MediaControllerExtraData.class);
- return extraData != null ? extraData.getMediaController() : null;
- } else if (android.os.Build.VERSION.SDK_INT >= 21) {
- Object controllerObj = MediaControllerCompatApi21.getMediaController(activity);
- if (controllerObj == null) {
- return null;
- }
- Object sessionTokenObj = MediaControllerCompatApi21.getSessionToken(controllerObj);
- try {
- return new MediaControllerCompat(activity,
- MediaSessionCompat.Token.fromToken(sessionTokenObj));
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getMediaController.", e);
- }
- }
- return null;
- }
-
- private static void validateCustomAction(String action, Bundle args) {
- if (action == null) {
- return;
- }
- switch(action) {
- case MediaSessionCompat.ACTION_FOLLOW:
- case MediaSessionCompat.ACTION_UNFOLLOW:
- if (args == null
- || !args.containsKey(MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE)) {
- throw new IllegalArgumentException("An extra field "
- + MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE + " is required "
- + "for this action " + action + ".");
- }
- break;
- }
- }
-
- private final MediaControllerImpl mImpl;
- private final MediaSessionCompat.Token mToken;
- // This set is used to keep references to registered callbacks to prevent them being GCed,
- // since we only keep weak references for callbacks in this class and its inner classes.
- private final HashSet<Callback> mRegisteredCallbacks = new HashSet<>();
-
- /**
- * Creates a media controller from a session.
- *
- * @param session The session to be controlled.
- */
- public MediaControllerCompat(Context context, @NonNull MediaSessionCompat session) {
- if (session == null) {
- throw new IllegalArgumentException("session must not be null");
- }
- mToken = session.getSessionToken();
-
- if (android.os.Build.VERSION.SDK_INT >= 24) {
- mImpl = new MediaControllerImplApi24(context, session);
- } else if (android.os.Build.VERSION.SDK_INT >= 23) {
- mImpl = new MediaControllerImplApi23(context, session);
- } else if (android.os.Build.VERSION.SDK_INT >= 21) {
- mImpl = new MediaControllerImplApi21(context, session);
- } else {
- mImpl = new MediaControllerImplBase(mToken);
- }
- }
-
- /**
- * Creates a media controller from a session token which may have
- * been obtained from another process.
- *
- * @param sessionToken The token of the session to be controlled.
- * @throws RemoteException if the session is not accessible.
- */
- public MediaControllerCompat(Context context, @NonNull MediaSessionCompat.Token sessionToken)
- throws RemoteException {
- if (sessionToken == null) {
- throw new IllegalArgumentException("sessionToken must not be null");
- }
- mToken = sessionToken;
-
- if (android.os.Build.VERSION.SDK_INT >= 24) {
- mImpl = new MediaControllerImplApi24(context, sessionToken);
- } else if (android.os.Build.VERSION.SDK_INT >= 23) {
- mImpl = new MediaControllerImplApi23(context, sessionToken);
- } else if (android.os.Build.VERSION.SDK_INT >= 21) {
- mImpl = new MediaControllerImplApi21(context, sessionToken);
- } else {
- mImpl = new MediaControllerImplBase(mToken);
- }
- }
-
- /**
- * Gets a {@link TransportControls} instance for this session.
- *
- * @return A controls instance
- */
- public TransportControls getTransportControls() {
- return mImpl.getTransportControls();
- }
-
- /**
- * Sends the specified media button event to the session. Only media keys can
- * be sent by this method, other keys will be ignored.
- *
- * @param keyEvent The media button event to dispatch.
- * @return true if the event was sent to the session, false otherwise.
- */
- public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) {
- if (keyEvent == null) {
- throw new IllegalArgumentException("KeyEvent may not be null");
- }
- return mImpl.dispatchMediaButtonEvent(keyEvent);
- }
-
- /**
- * Gets the current playback state for this session.
- *
- * <p>If the session is not ready, {@link PlaybackStateCompat#getExtras()} on the result of
- * this method may return null. </p>
- *
- * @return The current PlaybackState or null
- * @see #isSessionReady
- * @see Callback#onSessionReady
- */
- public PlaybackStateCompat getPlaybackState() {
- return mImpl.getPlaybackState();
- }
-
- /**
- * Gets the current metadata for this session.
- *
- * @return The current MediaMetadata or null.
- */
- public MediaMetadataCompat getMetadata() {
- return mImpl.getMetadata();
- }
-
- /**
- * Gets the current play queue for this session if one is set. If you only
- * care about the current item {@link #getMetadata()} should be used.
- *
- * @return The current play queue or null.
- */
- public List<QueueItem> getQueue() {
- return mImpl.getQueue();
- }
-
- /**
- * Adds a queue item from the given {@code description} at the end of the play queue
- * of this session. Not all sessions may support this. To know whether the session supports
- * this, get the session's flags with {@link #getFlags()} and check that the flag
- * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
- *
- * @param description The {@link MediaDescriptionCompat} for creating the
- * {@link MediaSessionCompat.QueueItem} to be inserted.
- * @throws UnsupportedOperationException If this session doesn't support this.
- * @see #getFlags()
- * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
- */
- public void addQueueItem(MediaDescriptionCompat description) {
- mImpl.addQueueItem(description);
- }
-
- /**
- * Adds a queue item from the given {@code description} at the specified position
- * in the play queue of this session. Shifts the queue item currently at that position
- * (if any) and any subsequent queue items to the right (adds one to their indices).
- * Not all sessions may support this. To know whether the session supports this,
- * get the session's flags with {@link #getFlags()} and check that the flag
- * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
- *
- * @param description The {@link MediaDescriptionCompat} for creating the
- * {@link MediaSessionCompat.QueueItem} to be inserted.
- * @param index The index at which the created {@link MediaSessionCompat.QueueItem}
- * is to be inserted.
- * @throws UnsupportedOperationException If this session doesn't support this.
- * @see #getFlags()
- * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
- */
- public void addQueueItem(MediaDescriptionCompat description, int index) {
- mImpl.addQueueItem(description, index);
- }
-
- /**
- * Removes the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
- * with the given {@link MediaDescriptionCompat description} in the play queue of the
- * associated session. Not all sessions may support this. To know whether the session supports
- * this, get the session's flags with {@link #getFlags()} and check that the flag
- * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
- *
- * @param description The {@link MediaDescriptionCompat} for denoting the
- * {@link MediaSessionCompat.QueueItem} to be removed.
- * @throws UnsupportedOperationException If this session doesn't support this.
- * @see #getFlags()
- * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
- */
- public void removeQueueItem(MediaDescriptionCompat description) {
- mImpl.removeQueueItem(description);
- }
-
- /**
- * Removes an queue item at the specified position in the play queue
- * of this session. Not all sessions may support this. To know whether the session supports
- * this, get the session's flags with {@link #getFlags()} and check that the flag
- * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
- *
- * @param index The index of the element to be removed.
- * @throws UnsupportedOperationException If this session doesn't support this.
- * @see #getFlags()
- * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
- * @deprecated Use {@link #removeQueueItem(MediaDescriptionCompat)} instead.
- */
- @Deprecated
- public void removeQueueItemAt(int index) {
- List<QueueItem> queue = getQueue();
- if (queue != null && index >= 0 && index < queue.size()) {
- QueueItem item = queue.get(index);
- if (item != null) {
- removeQueueItem(item.getDescription());
- }
- }
- }
-
- /**
- * Gets the queue title for this session.
- */
- public CharSequence getQueueTitle() {
- return mImpl.getQueueTitle();
- }
-
- /**
- * Gets the extras for this session.
- */
- public Bundle getExtras() {
- return mImpl.getExtras();
- }
-
- /**
- * Gets the rating type supported by the session. One of:
- * <ul>
- * <li>{@link RatingCompat#RATING_NONE}</li>
- * <li>{@link RatingCompat#RATING_HEART}</li>
- * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
- * <li>{@link RatingCompat#RATING_3_STARS}</li>
- * <li>{@link RatingCompat#RATING_4_STARS}</li>
- * <li>{@link RatingCompat#RATING_5_STARS}</li>
- * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
- * </ul>
- * <p>If the session is not ready, it will return {@link RatingCompat#RATING_NONE}.</p>
- *
- * @return The supported rating type, or {@link RatingCompat#RATING_NONE} if the value is not
- * set or the session is not ready.
- * @see #isSessionReady
- * @see Callback#onSessionReady
- */
- public int getRatingType() {
- return mImpl.getRatingType();
- }
-
- /**
- * Returns whether captioning is enabled for this session.
- *
- * <p>If the session is not ready, it will return a {@code false}.</p>
- *
- * @return {@code true} if captioning is enabled, {@code false} if disabled or not set.
- * @see #isSessionReady
- * @see Callback#onSessionReady
- */
- public boolean isCaptioningEnabled() {
- return mImpl.isCaptioningEnabled();
- }
-
- /**
- * Gets the repeat mode for this session.
- *
- * @return The latest repeat mode set to the session,
- * {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set, or
- * {@link PlaybackStateCompat#REPEAT_MODE_INVALID} if the session is not ready yet.
- * @see #isSessionReady
- * @see Callback#onSessionReady
- */
- public int getRepeatMode() {
- return mImpl.getRepeatMode();
- }
-
- /**
- * Gets the shuffle mode for this session.
- *
- * @return The latest shuffle mode set to the session, or
- * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE} if disabled or not set, or
- * {@link PlaybackStateCompat#SHUFFLE_MODE_INVALID} if the session is not ready yet.
- * @see #isSessionReady
- * @see Callback#onSessionReady
- */
- public int getShuffleMode() {
- return mImpl.getShuffleMode();
- }
-
- /**
- * Gets the flags for this session. Flags are defined in
- * {@link MediaSessionCompat}.
- *
- * @return The current set of flags for the session.
- */
- public long getFlags() {
- return mImpl.getFlags();
- }
-
- /**
- * Gets the current playback info for this session.
- *
- * @return The current playback info or null.
- */
- public PlaybackInfo getPlaybackInfo() {
- return mImpl.getPlaybackInfo();
- }
-
- /**
- * Gets an intent for launching UI associated with this session if one
- * exists.
- *
- * @return A {@link PendingIntent} to launch UI or null.
- */
- public PendingIntent getSessionActivity() {
- return mImpl.getSessionActivity();
- }
-
- /**
- * Gets the token for the session this controller is connected to.
- *
- * @return The session's token.
- */
- public MediaSessionCompat.Token getSessionToken() {
- return mToken;
- }
-
- /**
- * Sets the volume of the output this session is playing on. The command will
- * be ignored if it does not support
- * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
- * {@link AudioManager} may be used to affect the handling.
- *
- * @see #getPlaybackInfo()
- * @param value The value to set it to, between 0 and the reported max.
- * @param flags Flags from {@link AudioManager} to include with the volume
- * request.
- */
- public void setVolumeTo(int value, int flags) {
- mImpl.setVolumeTo(value, flags);
- }
-
- /**
- * Adjusts the volume of the output this session is playing on. The direction
- * must be one of {@link AudioManager#ADJUST_LOWER},
- * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
- * The command will be ignored if the session does not support
- * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
- * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
- * {@link AudioManager} may be used to affect the handling.
- *
- * @see #getPlaybackInfo()
- * @param direction The direction to adjust the volume in.
- * @param flags Any flags to pass with the command.
- */
- public void adjustVolume(int direction, int flags) {
- mImpl.adjustVolume(direction, flags);
- }
-
- /**
- * Adds a callback to receive updates from the Session. Updates will be
- * posted on the caller's thread.
- *
- * @param callback The callback object, must not be null.
- */
- public void registerCallback(@NonNull Callback callback) {
- registerCallback(callback, null);
- }
-
- /**
- * Adds a callback to receive updates from the session. Updates will be
- * posted on the specified handler's thread.
- *
- * @param callback The callback object, must not be null.
- * @param handler The handler to post updates on. If null the callers thread
- * will be used.
- */
- public void registerCallback(@NonNull Callback callback, Handler handler) {
- if (callback == null) {
- throw new IllegalArgumentException("callback must not be null");
- }
- if (handler == null) {
- handler = new Handler();
- }
- callback.setHandler(handler);
- mImpl.registerCallback(callback, handler);
- mRegisteredCallbacks.add(callback);
- }
-
- /**
- * Stops receiving updates on the specified callback. If an update has
- * already been posted you may still receive it after calling this method.
- *
- * @param callback The callback to remove
- */
- public void unregisterCallback(@NonNull Callback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("callback must not be null");
- }
- try {
- mRegisteredCallbacks.remove(callback);
- mImpl.unregisterCallback(callback);
- } finally {
- callback.setHandler(null);
- }
- }
-
- /**
- * Sends a generic command to the session. It is up to the session creator
- * to decide what commands and parameters they will support. As such,
- * commands should only be sent to sessions that the controller owns.
- *
- * @param command The command to send
- * @param params Any parameters to include with the command
- * @param cb The callback to receive the result on
- */
- public void sendCommand(@NonNull String command, Bundle params, ResultReceiver cb) {
- if (TextUtils.isEmpty(command)) {
- throw new IllegalArgumentException("command must neither be null nor empty");
- }
- mImpl.sendCommand(command, params, cb);
- }
-
- /**
- * Returns whether the session is ready or not.
- *
- * <p>If the session is not ready, following methods can work incorrectly.</p>
- * <ul>
- * <li>{@link #getPlaybackState()}</li>
- * <li>{@link #getRatingType()}</li>
- * <li>{@link #getRepeatMode()}</li>
- * <li>{@link #getShuffleMode()}</li>
- * <li>{@link #isCaptioningEnabled()}</li>
- * </ul>
- *
- * @return true if the session is ready, false otherwise.
- * @see Callback#onSessionReady()
- */
- public boolean isSessionReady() {
- return mImpl.isSessionReady();
- }
-
- /**
- * Gets the session owner's package name.
- *
- * @return The package name of of the session owner.
- */
- public String getPackageName() {
- return mImpl.getPackageName();
- }
-
- /**
- * Gets the underlying framework
- * {@link android.media.session.MediaController} object.
- * <p>
- * This method is only supported on API 21+.
- * </p>
- *
- * @return The underlying {@link android.media.session.MediaController}
- * object, or null if none.
- */
- public Object getMediaController() {
- return mImpl.getMediaController();
- }
-
- /**
- * Callback for receiving updates on from the session. A Callback can be
- * registered using {@link #registerCallback}
- */
- public static abstract class Callback implements IBinder.DeathRecipient {
- private final Object mCallbackObj;
- MessageHandler mHandler;
- boolean mHasExtraCallback;
-
- public Callback() {
- if (android.os.Build.VERSION.SDK_INT >= 21) {
- mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this));
- } else {
- mCallbackObj = new StubCompat(this);
- }
- }
-
- /**
- * Override to handle the session being ready.
- *
- * @see MediaControllerCompat#isSessionReady
- */
- public void onSessionReady() {
- }
-
- /**
- * Override to handle the session being destroyed. The session is no
- * longer valid after this call and calls to it will be ignored.
- */
- public void onSessionDestroyed() {
- }
-
- /**
- * Override to handle custom events sent by the session owner without a
- * specified interface. Controllers should only handle these for
- * sessions they own.
- *
- * @param event The event from the session.
- * @param extras Optional parameters for the event.
- */
- public void onSessionEvent(String event, Bundle extras) {
- }
-
- /**
- * Override to handle changes in playback state.
- *
- * @param state The new playback state of the session
- */
- public void onPlaybackStateChanged(PlaybackStateCompat state) {
- }
-
- /**
- * Override to handle changes to the current metadata.
- *
- * @param metadata The current metadata for the session or null if none.
- * @see MediaMetadataCompat
- */
- public void onMetadataChanged(MediaMetadataCompat metadata) {
- }
-
- /**
- * Override to handle changes to items in the queue.
- *
- * @see MediaSessionCompat.QueueItem
- * @param queue A list of items in the current play queue. It should
- * include the currently playing item as well as previous and
- * upcoming items if applicable.
- */
- public void onQueueChanged(List<QueueItem> queue) {
- }
-
- /**
- * Override to handle changes to the queue title.
- *
- * @param title The title that should be displayed along with the play
- * queue such as "Now Playing". May be null if there is no
- * such title.
- */
- public void onQueueTitleChanged(CharSequence title) {
- }
-
- /**
- * Override to handle changes to the {@link MediaSessionCompat} extras.
- *
- * @param extras The extras that can include other information
- * associated with the {@link MediaSessionCompat}.
- */
- public void onExtrasChanged(Bundle extras) {
- }
-
- /**
- * Override to handle changes to the audio info.
- *
- * @param info The current audio info for this session.
- */
- public void onAudioInfoChanged(PlaybackInfo info) {
- }
-
- /**
- * Override to handle changes to the captioning enabled status.
- *
- * @param enabled {@code true} if captioning is enabled, {@code false} otherwise.
- */
- public void onCaptioningEnabledChanged(boolean enabled) {
- }
-
- /**
- * Override to handle changes to the repeat mode.
- *
- * @param repeatMode The repeat mode. It should be one of followings:
- * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
- * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
- * {@link PlaybackStateCompat#REPEAT_MODE_ALL},
- * {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
- */
- public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
- }
-
- /**
- * Override to handle changes to the shuffle mode.
- *
- * @param shuffleMode The shuffle mode. Must be one of the followings:
- * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
- * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
- * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
- */
- public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
- }
-
- @Override
- public void binderDied() {
- onSessionDestroyed();
- }
-
- /**
- * Set the handler to use for callbacks.
- */
- void setHandler(Handler handler) {
- if (handler == null) {
- if (mHandler != null) {
- mHandler.mRegistered = false;
- mHandler.removeCallbacksAndMessages(null);
- mHandler = null;
- }
- } else {
- mHandler = new MessageHandler(handler.getLooper());
- mHandler.mRegistered = true;
- }
- }
-
- void postToHandler(int what, Object obj, Bundle data) {
- if (mHandler != null) {
- Message msg = mHandler.obtainMessage(what, obj);
- msg.setData(data);
- msg.sendToTarget();
- }
- }
-
- private static class StubApi21 implements MediaControllerCompatApi21.Callback {
- private final WeakReference<MediaControllerCompat.Callback> mCallback;
-
- StubApi21(MediaControllerCompat.Callback callback) {
- mCallback = new WeakReference<>(callback);
- }
-
- @Override
- public void onSessionDestroyed() {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.onSessionDestroyed();
- }
- }
-
- @Override
- public void onSessionEvent(String event, Bundle extras) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- if (callback.mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) {
- // Ignore. ExtraCallback will handle this.
- } else {
- callback.onSessionEvent(event, extras);
- }
- }
- }
-
- @Override
- public void onPlaybackStateChanged(Object stateObj) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- if (callback.mHasExtraCallback) {
- // Ignore. ExtraCallback will handle this.
- } else {
- callback.onPlaybackStateChanged(
- PlaybackStateCompat.fromPlaybackState(stateObj));
- }
- }
- }
-
- @Override
- public void onMetadataChanged(Object metadataObj) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj));
- }
- }
-
- @Override
- public void onQueueChanged(List<?> queue) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.onQueueChanged(QueueItem.fromQueueItemList(queue));
- }
- }
-
- @Override
- public void onQueueTitleChanged(CharSequence title) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.onQueueTitleChanged(title);
- }
- }
-
- @Override
- public void onExtrasChanged(Bundle extras) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.onExtrasChanged(extras);
- }
- }
-
- @Override
- public void onAudioInfoChanged(
- int type, int stream, int control, int max, int current) {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.onAudioInfoChanged(
- new PlaybackInfo(type, stream, control, max, current));
- }
- }
- }
-
- private static class StubCompat extends IMediaControllerCallback.Stub {
- private final WeakReference<MediaControllerCompat.Callback> mCallback;
-
- StubCompat(MediaControllerCompat.Callback callback) {
- mCallback = new WeakReference<>(callback);
- }
-
- @Override
- public void onEvent(String event, Bundle extras) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_EVENT, event, extras);
- }
- }
-
- @Override
- public void onSessionDestroyed() throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_DESTROYED, null, null);
- }
- }
-
- @Override
- public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
- }
- }
-
- @Override
- public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
- }
- }
-
- @Override
- public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
- }
- }
-
- @Override
- public void onQueueTitleChanged(CharSequence title) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
- }
- }
-
- @Override
- public void onCaptioningEnabledChanged(boolean enabled) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(
- MessageHandler.MSG_UPDATE_CAPTIONING_ENABLED, enabled, null);
- }
- }
-
- @Override
- public void onRepeatModeChanged(int repeatMode) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
- }
- }
-
- @Override
- public void onShuffleModeChangedRemoved(boolean enabled) throws RemoteException {
- // Do nothing.
- }
-
- @Override
- public void onShuffleModeChanged(int shuffleMode) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(
- MessageHandler.MSG_UPDATE_SHUFFLE_MODE, shuffleMode, null);
- }
- }
-
- @Override
- public void onExtrasChanged(Bundle extras) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
- }
- }
-
- @Override
- public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- PlaybackInfo pi = null;
- if (info != null) {
- pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
- info.maxVolume, info.currentVolume);
- }
- callback.postToHandler(MessageHandler.MSG_UPDATE_VOLUME, pi, null);
- }
- }
-
- @Override
- public void onSessionReady() throws RemoteException {
- MediaControllerCompat.Callback callback = mCallback.get();
- if (callback != null) {
- callback.postToHandler(MessageHandler.MSG_SESSION_READY, null, null);
- }
- }
- }
-
- private class MessageHandler extends Handler {
- private static final int MSG_EVENT = 1;
- private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
- private static final int MSG_UPDATE_METADATA = 3;
- private static final int MSG_UPDATE_VOLUME = 4;
- private static final int MSG_UPDATE_QUEUE = 5;
- private static final int MSG_UPDATE_QUEUE_TITLE = 6;
- private static final int MSG_UPDATE_EXTRAS = 7;
- private static final int MSG_DESTROYED = 8;
- private static final int MSG_UPDATE_REPEAT_MODE = 9;
- private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11;
- private static final int MSG_UPDATE_SHUFFLE_MODE = 12;
- private static final int MSG_SESSION_READY = 13;
-
- boolean mRegistered = false;
-
- MessageHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (!mRegistered) {
- return;
- }
- switch (msg.what) {
- case MSG_EVENT:
- onSessionEvent((String) msg.obj, msg.getData());
- break;
- case MSG_UPDATE_PLAYBACK_STATE:
- onPlaybackStateChanged((PlaybackStateCompat) msg.obj);
- break;
- case MSG_UPDATE_METADATA:
- onMetadataChanged((MediaMetadataCompat) msg.obj);
- break;
- case MSG_UPDATE_QUEUE:
- onQueueChanged((List<QueueItem>) msg.obj);
- break;
- case MSG_UPDATE_QUEUE_TITLE:
- onQueueTitleChanged((CharSequence) msg.obj);
- break;
- case MSG_UPDATE_CAPTIONING_ENABLED:
- onCaptioningEnabledChanged((boolean) msg.obj);
- break;
- case MSG_UPDATE_REPEAT_MODE:
- onRepeatModeChanged((int) msg.obj);
- break;
- case MSG_UPDATE_SHUFFLE_MODE:
- onShuffleModeChanged((int) msg.obj);
- break;
- case MSG_UPDATE_EXTRAS:
- onExtrasChanged((Bundle) msg.obj);
- break;
- case MSG_UPDATE_VOLUME:
- onAudioInfoChanged((PlaybackInfo) msg.obj);
- break;
- case MSG_DESTROYED:
- onSessionDestroyed();
- break;
- case MSG_SESSION_READY:
- onSessionReady();
- break;
- }
- }
- }
- }
-
- /**
- * Interface for controlling media playback on a session. This allows an app
- * to send media transport commands to the session.
- */
- public static abstract class TransportControls {
- /**
- * Used as an integer extra field in {@link #playFromMediaId(String, Bundle)} or
- * {@link #prepareFromMediaId(String, Bundle)} to indicate the stream type to be used by the
- * media player when playing or preparing the specified media id. See {@link AudioManager}
- * for a list of stream types.
- */
- public static final String EXTRA_LEGACY_STREAM_TYPE =
- "android.media.session.extra.LEGACY_STREAM_TYPE";
-
- TransportControls() {
- }
-
- /**
- * Request that the player prepare for playback. This can decrease the time it takes to
- * start playback when a play command is received. Preparation is not required. You can
- * call {@link #play} without calling this method beforehand.
- */
- public abstract void prepare();
-
- /**
- * Request that the player prepare playback for a specific media id. This can decrease the
- * time it takes to start playback when a play command is received. Preparation is not
- * required. You can call {@link #playFromMediaId} without calling this method beforehand.
- *
- * @param mediaId The id of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be prepared.
- */
- public abstract void prepareFromMediaId(String mediaId, Bundle extras);
-
- /**
- * Request that the player prepare playback for a specific search query. This can decrease
- * the time it takes to start playback when a play command is received. An empty or null
- * query should be treated as a request to prepare any music. Preparation is not required.
- * You can call {@link #playFromSearch} without calling this method beforehand.
- *
- * @param query The search query.
- * @param extras Optional extras that can include extra information
- * about the query.
- */
- public abstract void prepareFromSearch(String query, Bundle extras);
-
- /**
- * Request that the player prepare playback for a specific {@link Uri}. This can decrease
- * the time it takes to start playback when a play command is received. Preparation is not
- * required. You can call {@link #playFromUri} without calling this method beforehand.
- *
- * @param uri The URI of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be prepared.
- */
- public abstract void prepareFromUri(Uri uri, Bundle extras);
-
- /**
- * Request that the player start its playback at its current position.
- */
- public abstract void play();
-
- /**
- * Request that the player start playback for a specific {@link Uri}.
- *
- * @param mediaId The uri of the requested media.
- * @param extras Optional extras that can include extra information
- * about the media item to be played.
- */
- public abstract void playFromMediaId(String mediaId, Bundle extras);
-
- /**
- * Request that the player start playback for a specific search query.
- * An empty or null query should be treated as a request to play any
- * music.
- *
- * @param query The search query.
- * @param extras Optional extras that can include extra information
- * about the query.
- */
- public abstract void playFromSearch(String query, Bundle extras);
-
- /**
- * Request that the player start playback for a specific {@link Uri}.
- *
- * @param uri The URI of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be played.
- */
- public abstract void playFromUri(Uri uri, Bundle extras);
-
- /**
- * Plays an item with a specific id in the play queue. If you specify an
- * id that is not in the play queue, the behavior is undefined.
- */
- public abstract void skipToQueueItem(long id);
-
- /**
- * Request that the player pause its playback and stay at its current
- * position.
- */
- public abstract void pause();
-
- /**
- * Request that the player stop its playback; it may clear its state in
- * whatever way is appropriate.
- */
- public abstract void stop();
-
- /**
- * Moves to a new location in the media stream.
- *
- * @param pos Position to move to, in milliseconds.
- */
- public abstract void seekTo(long pos);
-
- /**
- * Starts fast forwarding. If playback is already fast forwarding this
- * may increase the rate.
- */
- public abstract void fastForward();
-
- /**
- * Skips to the next item.
- */
- public abstract void skipToNext();
-
- /**
- * Starts rewinding. If playback is already rewinding this may increase
- * the rate.
- */
- public abstract void rewind();
-
- /**
- * Skips to the previous item.
- */
- public abstract void skipToPrevious();
-
- /**
- * Rates the current content. This will cause the rating to be set for
- * the current user. The rating type of the given {@link RatingCompat} must match the type
- * returned by {@link #getRatingType()}.
- *
- * @param rating The rating to set for the current content
- */
- public abstract void setRating(RatingCompat rating);
-
- /**
- * Rates a media item. This will cause the rating to be set for
- * the specific media item. The rating type of the given {@link RatingCompat} must match
- * the type returned by {@link #getRatingType()}.
- *
- * @param rating The rating to set for the media item.
- * @param extras Optional arguments that can include information about the media item
- * to be rated.
- *
- * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE
- * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE_VALUE
- */
- public abstract void setRating(RatingCompat rating, Bundle extras);
-
- /**
- * Enables/disables captioning for this session.
- *
- * @param enabled {@code true} to enable captioning, {@code false} to disable.
- */
- public abstract void setCaptioningEnabled(boolean enabled);
-
- /**
- * Sets the repeat mode for this session.
- *
- * @param repeatMode The repeat mode. Must be one of the followings:
- * {@link PlaybackStateCompat#REPEAT_MODE_NONE},
- * {@link PlaybackStateCompat#REPEAT_MODE_ONE},
- * {@link PlaybackStateCompat#REPEAT_MODE_ALL},
- * {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
- */
- public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
-
- /**
- * Sets the shuffle mode for this session.
- *
- * @param shuffleMode The shuffle mode. Must be one of the followings:
- * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
- * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
- * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
- */
- public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode);
-
- /**
- * Sends a custom action for the {@link MediaSessionCompat} to perform.
- *
- * @param customAction The action to perform.
- * @param args Optional arguments to supply to the
- * {@link MediaSessionCompat} for this custom action.
- */
- public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction,
- Bundle args);
-
- /**
- * Sends the id and args from a custom action for the
- * {@link MediaSessionCompat} to perform.
- *
- * @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
- * Bundle args)
- * @see MediaSessionCompat#ACTION_FLAG_AS_INAPPROPRIATE
- * @see MediaSessionCompat#ACTION_SKIP_AD
- * @see MediaSessionCompat#ACTION_FOLLOW
- * @see MediaSessionCompat#ACTION_UNFOLLOW
- * @param action The action identifier of the
- * {@link PlaybackStateCompat.CustomAction} as specified by
- * the {@link MediaSessionCompat}.
- * @param args Optional arguments to supply to the
- * {@link MediaSessionCompat} for this custom action.
- */
- public abstract void sendCustomAction(String action, Bundle args);
- }
-
- /**
- * Holds information about the way volume is handled for this session.
- */
- public static final class PlaybackInfo {
- /**
- * The session uses local playback.
- */
- public static final int PLAYBACK_TYPE_LOCAL = 1;
- /**
- * The session uses remote playback.
- */
- public static final int PLAYBACK_TYPE_REMOTE = 2;
-
- private final int mPlaybackType;
- // TODO update audio stream with AudioAttributes support version
- private final int mAudioStream;
- private final int mVolumeControl;
- private final int mMaxVolume;
- private final int mCurrentVolume;
-
- PlaybackInfo(int type, int stream, int control, int max, int current) {
- mPlaybackType = type;
- mAudioStream = stream;
- mVolumeControl = control;
- mMaxVolume = max;
- mCurrentVolume = current;
- }
-
- /**
- * Gets the type of volume handling, either local or remote. One of:
- * <ul>
- * <li>{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}</li>
- * <li>{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}</li>
- * </ul>
- *
- * @return The type of volume handling this session is using.
- */
- public int getPlaybackType() {
- return mPlaybackType;
- }
-
- /**
- * Gets the stream this is currently controlling volume on. When the volume
- * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
- * have meaning and should be ignored.
- *
- * @return The stream this session is playing on.
- */
- public int getAudioStream() {
- // TODO switch to AudioAttributesCompat when it is added.
- return mAudioStream;
- }
-
- /**
- * Gets the type of volume control that can be used. One of:
- * <ul>
- * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
- * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
- * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
- * </ul>
- *
- * @return The type of volume control that may be used with this
- * session.
- */
- public int getVolumeControl() {
- return mVolumeControl;
- }
-
- /**
- * Gets the maximum volume that may be set for this session.
- *
- * @return The maximum allowed volume where this session is playing.
- */
- public int getMaxVolume() {
- return mMaxVolume;
- }
-
- /**
- * Gets the current volume for this session.
- *
- * @return The current volume where this session is playing.
- */
- public int getCurrentVolume() {
- return mCurrentVolume;
- }
- }
-
- interface MediaControllerImpl {
- void registerCallback(Callback callback, Handler handler);
-
- void unregisterCallback(Callback callback);
- boolean dispatchMediaButtonEvent(KeyEvent keyEvent);
- TransportControls getTransportControls();
- PlaybackStateCompat getPlaybackState();
- MediaMetadataCompat getMetadata();
-
- List<QueueItem> getQueue();
- void addQueueItem(MediaDescriptionCompat description);
- void addQueueItem(MediaDescriptionCompat description, int index);
- void removeQueueItem(MediaDescriptionCompat description);
- CharSequence getQueueTitle();
- Bundle getExtras();
- int getRatingType();
- boolean isCaptioningEnabled();
- int getRepeatMode();
- int getShuffleMode();
- long getFlags();
- PlaybackInfo getPlaybackInfo();
- PendingIntent getSessionActivity();
-
- void setVolumeTo(int value, int flags);
- void adjustVolume(int direction, int flags);
- void sendCommand(String command, Bundle params, ResultReceiver cb);
-
- boolean isSessionReady();
- String getPackageName();
- Object getMediaController();
- }
-
- static class MediaControllerImplBase implements MediaControllerImpl {
- private IMediaSession mBinder;
- private TransportControls mTransportControls;
-
- public MediaControllerImplBase(MediaSessionCompat.Token token) {
- mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken());
- }
-
- @Override
- public void registerCallback(Callback callback, Handler handler) {
- if (callback == null) {
- throw new IllegalArgumentException("callback may not be null.");
- }
- try {
- mBinder.asBinder().linkToDeath(callback, 0);
- mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in registerCallback.", e);
- callback.onSessionDestroyed();
- }
- }
-
- @Override
- public void unregisterCallback(Callback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("callback may not be null.");
- }
- try {
- mBinder.unregisterCallbackListener(
- (IMediaControllerCallback) callback.mCallbackObj);
- mBinder.asBinder().unlinkToDeath(callback, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in unregisterCallback.", e);
- }
- }
-
- @Override
- public boolean dispatchMediaButtonEvent(KeyEvent event) {
- if (event == null) {
- throw new IllegalArgumentException("event may not be null.");
- }
- try {
- mBinder.sendMediaButton(event);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in dispatchMediaButtonEvent.", e);
- }
- return false;
- }
-
- @Override
- public TransportControls getTransportControls() {
- if (mTransportControls == null) {
- mTransportControls = new TransportControlsBase(mBinder);
- }
-
- return mTransportControls;
- }
-
- @Override
- public PlaybackStateCompat getPlaybackState() {
- try {
- return mBinder.getPlaybackState();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getPlaybackState.", e);
- }
- return null;
- }
-
- @Override
- public MediaMetadataCompat getMetadata() {
- try {
- return mBinder.getMetadata();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getMetadata.", e);
- }
- return null;
- }
-
- @Override
- public List<QueueItem> getQueue() {
- try {
- return mBinder.getQueue();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getQueue.", e);
- }
- return null;
- }
-
- @Override
- public void addQueueItem(MediaDescriptionCompat description) {
- try {
- long flags = mBinder.getFlags();
- if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
- throw new UnsupportedOperationException(
- "This session doesn't support queue management operations");
- }
- mBinder.addQueueItem(description);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in addQueueItem.", e);
- }
- }
-
- @Override
- public void addQueueItem(MediaDescriptionCompat description, int index) {
- try {
- long flags = mBinder.getFlags();
- if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
- throw new UnsupportedOperationException(
- "This session doesn't support queue management operations");
- }
- mBinder.addQueueItemAt(description, index);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in addQueueItemAt.", e);
- }
- }
-
- @Override
- public void removeQueueItem(MediaDescriptionCompat description) {
- try {
- long flags = mBinder.getFlags();
- if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
- throw new UnsupportedOperationException(
- "This session doesn't support queue management operations");
- }
- mBinder.removeQueueItem(description);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in removeQueueItem.", e);
- }
- }
-
- @Override
- public CharSequence getQueueTitle() {
- try {
- return mBinder.getQueueTitle();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getQueueTitle.", e);
- }
- return null;
- }
-
- @Override
- public Bundle getExtras() {
- try {
- return mBinder.getExtras();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getExtras.", e);
- }
- return null;
- }
-
- @Override
- public int getRatingType() {
- try {
- return mBinder.getRatingType();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getRatingType.", e);
- }
- return 0;
- }
-
- @Override
- public boolean isCaptioningEnabled() {
- try {
- return mBinder.isCaptioningEnabled();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
- }
- return false;
- }
-
- @Override
- public int getRepeatMode() {
- try {
- return mBinder.getRepeatMode();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getRepeatMode.", e);
- }
- return PlaybackStateCompat.REPEAT_MODE_INVALID;
- }
-
- @Override
- public int getShuffleMode() {
- try {
- return mBinder.getShuffleMode();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getShuffleMode.", e);
- }
- return PlaybackStateCompat.SHUFFLE_MODE_INVALID;
- }
-
- @Override
- public long getFlags() {
- try {
- return mBinder.getFlags();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getFlags.", e);
- }
- return 0;
- }
-
- @Override
- public PlaybackInfo getPlaybackInfo() {
- try {
- ParcelableVolumeInfo info = mBinder.getVolumeAttributes();
- PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream,
- info.controlType, info.maxVolume, info.currentVolume);
- return pi;
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getPlaybackInfo.", e);
- }
- return null;
- }
-
- @Override
- public PendingIntent getSessionActivity() {
- try {
- return mBinder.getLaunchPendingIntent();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getSessionActivity.", e);
- }
- return null;
- }
-
- @Override
- public void setVolumeTo(int value, int flags) {
- try {
- mBinder.setVolumeTo(value, flags, null);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setVolumeTo.", e);
- }
- }
-
- @Override
- public void adjustVolume(int direction, int flags) {
- try {
- mBinder.adjustVolume(direction, flags, null);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in adjustVolume.", e);
- }
- }
-
- @Override
- public void sendCommand(String command, Bundle params, ResultReceiver cb) {
- try {
- mBinder.sendCommand(command, params,
- new MediaSessionCompat.ResultReceiverWrapper(cb));
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in sendCommand.", e);
- }
- }
-
- @Override
- public boolean isSessionReady() {
- return true;
- }
-
- @Override
- public String getPackageName() {
- try {
- return mBinder.getPackageName();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getPackageName.", e);
- }
- return null;
- }
-
- @Override
- public Object getMediaController() {
- return null;
- }
- }
-
- static class TransportControlsBase extends TransportControls {
- private IMediaSession mBinder;
-
- public TransportControlsBase(IMediaSession binder) {
- mBinder = binder;
- }
-
- @Override
- public void prepare() {
- try {
- mBinder.prepare();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in prepare.", e);
- }
- }
-
- @Override
- public void prepareFromMediaId(String mediaId, Bundle extras) {
- try {
- mBinder.prepareFromMediaId(mediaId, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in prepareFromMediaId.", e);
- }
- }
-
- @Override
- public void prepareFromSearch(String query, Bundle extras) {
- try {
- mBinder.prepareFromSearch(query, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in prepareFromSearch.", e);
- }
- }
-
- @Override
- public void prepareFromUri(Uri uri, Bundle extras) {
- try {
- mBinder.prepareFromUri(uri, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in prepareFromUri.", e);
- }
- }
-
- @Override
- public void play() {
- try {
- mBinder.play();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in play.", e);
- }
- }
-
- @Override
- public void playFromMediaId(String mediaId, Bundle extras) {
- try {
- mBinder.playFromMediaId(mediaId, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in playFromMediaId.", e);
- }
- }
-
- @Override
- public void playFromSearch(String query, Bundle extras) {
- try {
- mBinder.playFromSearch(query, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in playFromSearch.", e);
- }
- }
-
- @Override
- public void playFromUri(Uri uri, Bundle extras) {
- try {
- mBinder.playFromUri(uri, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in playFromUri.", e);
- }
- }
-
- @Override
- public void skipToQueueItem(long id) {
- try {
- mBinder.skipToQueueItem(id);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in skipToQueueItem.", e);
- }
- }
-
- @Override
- public void pause() {
- try {
- mBinder.pause();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in pause.", e);
- }
- }
-
- @Override
- public void stop() {
- try {
- mBinder.stop();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in stop.", e);
- }
- }
-
- @Override
- public void seekTo(long pos) {
- try {
- mBinder.seekTo(pos);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in seekTo.", e);
- }
- }
-
- @Override
- public void fastForward() {
- try {
- mBinder.fastForward();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in fastForward.", e);
- }
- }
-
- @Override
- public void skipToNext() {
- try {
- mBinder.next();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in skipToNext.", e);
- }
- }
-
- @Override
- public void rewind() {
- try {
- mBinder.rewind();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in rewind.", e);
- }
- }
-
- @Override
- public void skipToPrevious() {
- try {
- mBinder.previous();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in skipToPrevious.", e);
- }
- }
-
- @Override
- public void setRating(RatingCompat rating) {
- try {
- mBinder.rate(rating);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setRating.", e);
- }
- }
-
- @Override
- public void setRating(RatingCompat rating, Bundle extras) {
- try {
- mBinder.rateWithExtras(rating, extras);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setRating.", e);
- }
- }
-
- @Override
- public void setCaptioningEnabled(boolean enabled) {
- try {
- mBinder.setCaptioningEnabled(enabled);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setCaptioningEnabled.", e);
- }
- }
-
- @Override
- public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
- try {
- mBinder.setRepeatMode(repeatMode);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setRepeatMode.", e);
- }
- }
-
- @Override
- public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
- try {
- mBinder.setShuffleMode(shuffleMode);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in setShuffleMode.", e);
- }
- }
-
- @Override
- public void sendCustomAction(CustomAction customAction, Bundle args) {
- sendCustomAction(customAction.getAction(), args);
- }
-
- @Override
- public void sendCustomAction(String action, Bundle args) {
- validateCustomAction(action, args);
- try {
- mBinder.sendCustomAction(action, args);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in sendCustomAction.", e);
- }
- }
- }
-
- @RequiresApi(21)
- static class MediaControllerImplApi21 implements MediaControllerImpl {
- protected final Object mControllerObj;
-
- private final List<Callback> mPendingCallbacks = new ArrayList<>();
-
- // Extra binder is used for applying the framework change of new APIs and bug fixes
- // after API 21.
- private IMediaSession mExtraBinder;
- private HashMap<Callback, ExtraCallback> mCallbackMap = new HashMap<>();
-
- public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
- mControllerObj = MediaControllerCompatApi21.fromToken(context,
- session.getSessionToken().getToken());
- mExtraBinder = session.getSessionToken().getExtraBinder();
- if (mExtraBinder == null) {
- requestExtraBinder();
- }
- }
-
- public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
- throws RemoteException {
- mControllerObj = MediaControllerCompatApi21.fromToken(context,
- sessionToken.getToken());
- if (mControllerObj == null) throw new RemoteException();
- mExtraBinder = sessionToken.getExtraBinder();
- if (mExtraBinder == null) {
- requestExtraBinder();
- }
- }
-
- @Override
- public final void registerCallback(Callback callback, Handler handler) {
- MediaControllerCompatApi21.registerCallback(
- mControllerObj, callback.mCallbackObj, handler);
- if (mExtraBinder != null) {
- ExtraCallback extraCallback = new ExtraCallback(callback);
- mCallbackMap.put(callback, extraCallback);
- callback.mHasExtraCallback = true;
- try {
- mExtraBinder.registerCallbackListener(extraCallback);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in registerCallback.", e);
- }
- } else {
- synchronized (mPendingCallbacks) {
- callback.mHasExtraCallback = false;
- mPendingCallbacks.add(callback);
- }
- }
- }
-
- @Override
- public final void unregisterCallback(Callback callback) {
- MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
- if (mExtraBinder != null) {
- try {
- ExtraCallback extraCallback = mCallbackMap.remove(callback);
- if (extraCallback != null) {
- callback.mHasExtraCallback = false;
- mExtraBinder.unregisterCallbackListener(extraCallback);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in unregisterCallback.", e);
- }
- } else {
- synchronized (mPendingCallbacks) {
- mPendingCallbacks.remove(callback);
- }
- }
- }
-
- @Override
- public boolean dispatchMediaButtonEvent(KeyEvent event) {
- return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
- }
-
- @Override
- public TransportControls getTransportControls() {
- Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
- return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
- }
-
- @Override
- public PlaybackStateCompat getPlaybackState() {
- if (mExtraBinder != null) {
- try {
- return mExtraBinder.getPlaybackState();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getPlaybackState.", e);
- }
- }
- Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
- return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
- }
-
- @Override
- public MediaMetadataCompat getMetadata() {
- Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
- return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
- }
-
- @Override
- public List<QueueItem> getQueue() {
- List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
- return queueObjs != null ? QueueItem.fromQueueItemList(queueObjs) : null;
- }
-
- @Override
- public void addQueueItem(MediaDescriptionCompat description) {
- long flags = getFlags();
- if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
- throw new UnsupportedOperationException(
- "This session doesn't support queue management operations");
- }
- Bundle params = new Bundle();
- params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
- sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null);
- }
-
- @Override
- public void addQueueItem(MediaDescriptionCompat description, int index) {
- long flags = getFlags();
- if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
- throw new UnsupportedOperationException(
- "This session doesn't support queue management operations");
- }
- Bundle params = new Bundle();
- params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
- params.putInt(COMMAND_ARGUMENT_INDEX, index);
- sendCommand(COMMAND_ADD_QUEUE_ITEM_AT, params, null);
- }
-
- @Override
- public void removeQueueItem(MediaDescriptionCompat description) {
- long flags = getFlags();
- if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
- throw new UnsupportedOperationException(
- "This session doesn't support queue management operations");
- }
- Bundle params = new Bundle();
- params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
- sendCommand(COMMAND_REMOVE_QUEUE_ITEM, params, null);
- }
-
- @Override
- public CharSequence getQueueTitle() {
- return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
- }
-
- @Override
- public Bundle getExtras() {
- return MediaControllerCompatApi21.getExtras(mControllerObj);
- }
-
- @Override
- public int getRatingType() {
- if (android.os.Build.VERSION.SDK_INT < 22 && mExtraBinder != null) {
- try {
- return mExtraBinder.getRatingType();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getRatingType.", e);
- }
- }
- return MediaControllerCompatApi21.getRatingType(mControllerObj);
- }
-
- @Override
- public boolean isCaptioningEnabled() {
- if (mExtraBinder != null) {
- try {
- return mExtraBinder.isCaptioningEnabled();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
- }
- }
- return false;
- }
-
- @Override
- public int getRepeatMode() {
- if (mExtraBinder != null) {
- try {
- return mExtraBinder.getRepeatMode();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getRepeatMode.", e);
- }
- }
- return PlaybackStateCompat.REPEAT_MODE_INVALID;
- }
-
- @Override
- public int getShuffleMode() {
- if (mExtraBinder != null) {
- try {
- return mExtraBinder.getShuffleMode();
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in getShuffleMode.", e);
- }
- }
- return PlaybackStateCompat.SHUFFLE_MODE_INVALID;
- }
-
- @Override
- public long getFlags() {
- return MediaControllerCompatApi21.getFlags(mControllerObj);
- }
-
- @Override
- public PlaybackInfo getPlaybackInfo() {
- Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj);
- return volumeInfoObj != null ? new PlaybackInfo(
- MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj),
- MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj),
- MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj),
- MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj),
- MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null;
- }
-
- @Override
- public PendingIntent getSessionActivity() {
- return MediaControllerCompatApi21.getSessionActivity(mControllerObj);
- }
-
- @Override
- public void setVolumeTo(int value, int flags) {
- MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags);
- }
-
- @Override
- public void adjustVolume(int direction, int flags) {
- MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags);
- }
-
- @Override
- public void sendCommand(String command, Bundle params, ResultReceiver cb) {
- MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb);
- }
-
- @Override
- public boolean isSessionReady() {
- return mExtraBinder != null;
- }
-
- @Override
- public String getPackageName() {
- return MediaControllerCompatApi21.getPackageName(mControllerObj);
- }
-
- @Override
- public Object getMediaController() {
- return mControllerObj;
- }
-
- private void requestExtraBinder() {
- sendCommand(COMMAND_GET_EXTRA_BINDER, null,
- new ExtraBinderRequestResultReceiver(this, new Handler()));
- }
-
- private void processPendingCallbacks() {
- if (mExtraBinder == null) {
- return;
- }
- synchronized (mPendingCallbacks) {
- for (Callback callback : mPendingCallbacks) {
- ExtraCallback extraCallback = new ExtraCallback(callback);
- mCallbackMap.put(callback, extraCallback);
- callback.mHasExtraCallback = true;
- try {
- mExtraBinder.registerCallbackListener(extraCallback);
- } catch (RemoteException e) {
- Log.e(TAG, "Dead object in registerCallback.", e);
- break;
- }
- callback.onSessionReady();
- }
- mPendingCallbacks.clear();
- }
- }
-
- private static class ExtraBinderRequestResultReceiver extends ResultReceiver {
- private WeakReference<MediaControllerImplApi21> mMediaControllerImpl;
-
- public ExtraBinderRequestResultReceiver(MediaControllerImplApi21 mediaControllerImpl,
- Handler handler) {
- super(handler);
- mMediaControllerImpl = new WeakReference<>(mediaControllerImpl);
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- MediaControllerImplApi21 mediaControllerImpl = mMediaControllerImpl.get();
- if (mediaControllerImpl == null || resultData == null) {
- return;
- }
- mediaControllerImpl.mExtraBinder = IMediaSession.Stub.asInterface(
- BundleCompat.getBinder(resultData, MediaSessionCompat.EXTRA_BINDER));
- mediaControllerImpl.processPendingCallbacks();
- }
- }
-
- private static class ExtraCallback extends Callback.StubCompat {
- ExtraCallback(Callback callback) {
- super(callback);
- }
-
- @Override
- public void onSessionDestroyed() throws RemoteException {
- // Will not be called.
- throw new AssertionError();
- }
-
- @Override
- public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
- // Will not be called.
- throw new AssertionError();
- }
-
- @Override
- public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
- // Will not be called.
- throw new AssertionError();
- }
-
- @Override
- public void onQueueTitleChanged(CharSequence title) throws RemoteException {
- // Will not be called.
- throw new AssertionError();
- }
-
- @Override
- public void onExtrasChanged(Bundle extras) throws RemoteException {
- // Will not be called.
- throw new AssertionError();
- }
-
- @Override
- public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
- // Will not be called.
- throw new AssertionError();
- }
- }
- }
-
- static class TransportControlsApi21 extends TransportControls {
- protected final Object mControlsObj;
-
- public TransportControlsApi21(Object controlsObj) {
- mControlsObj = controlsObj;
- }
-
- @Override
- public void prepare() {
- sendCustomAction(MediaSessionCompat.ACTION_PREPARE, null);
- }
-
- @Override
- public void prepareFromMediaId(String mediaId, Bundle extras) {
- Bundle bundle = new Bundle();
- bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ID, mediaId);
- bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
- sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_MEDIA_ID, bundle);
- }
-
- @Override
- public void prepareFromSearch(String query, Bundle extras) {
- Bundle bundle = new Bundle();
- bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_QUERY, query);
- bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
- sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_SEARCH, bundle);
- }
-
- @Override
- public void prepareFromUri(Uri uri, Bundle extras) {
- Bundle bundle = new Bundle();
- bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
- bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
- sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_URI, bundle);
- }
-
- @Override
- public void play() {
- MediaControllerCompatApi21.TransportControls.play(mControlsObj);
- }
-
- @Override
- public void pause() {
- MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
- }
-
- @Override
- public void stop() {
- MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
- }
-
- @Override
- public void seekTo(long pos) {
- MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
- }
-
- @Override
- public void fastForward() {
- MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
- }
-
- @Override
- public void rewind() {
- MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
- }
-
- @Override
- public void skipToNext() {
- MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
- }
-
- @Override
- public void skipToPrevious() {
- MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
- }
-
- @Override
- public void setRating(RatingCompat rating) {
- MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
- rating != null ? rating.getRating() : null);
- }
-
- @Override
- public void setRating(RatingCompat rating, Bundle extras) {
- Bundle bundle = new Bundle();
- bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_RATING, rating);
- bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
- sendCustomAction(MediaSessionCompat.ACTION_SET_RATING, bundle);
- }
-
- @Override
- public void setCaptioningEnabled(boolean enabled) {
- Bundle bundle = new Bundle();
- bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_CAPTIONING_ENABLED, enabled);
- sendCustomAction(MediaSessionCompat.ACTION_SET_CAPTIONING_ENABLED, bundle);
- }
-
- @Override
- public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
- Bundle bundle = new Bundle();
- bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
- sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
- }
-
- @Override
- public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
- Bundle bundle = new Bundle();
- bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE, shuffleMode);
- sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE, bundle);
- }
-
- @Override
- public void playFromMediaId(String mediaId, Bundle extras) {
- MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
- extras);
- }
-
- @Override
- public void playFromSearch(String query, Bundle extras) {
- MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query,
- extras);
- }
-
- @Override
- public void playFromUri(Uri uri, Bundle extras) {
- if (uri == null || Uri.EMPTY.equals(uri)) {
- throw new IllegalArgumentException(
- "You must specify a non-empty Uri for playFromUri.");
- }
- Bundle bundle = new Bundle();
- bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
- bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
- sendCustomAction(MediaSessionCompat.ACTION_PLAY_FROM_URI, bundle);
- }
-
- @Override
- public void skipToQueueItem(long id) {
- MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id);
- }
-
- @Override
- public void sendCustomAction(CustomAction customAction, Bundle args) {
- validateCustomAction(customAction.getAction(), args);
- MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
- customAction.getAction(), args);
- }
-
- @Override
- public void sendCustomAction(String action, Bundle args) {
- validateCustomAction(action, args);
- MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action,
- args);
- }
- }
-
- @RequiresApi(23)
- static class MediaControllerImplApi23 extends MediaControllerImplApi21 {
-
- public MediaControllerImplApi23(Context context, MediaSessionCompat session) {
- super(context, session);
- }
-
- public MediaControllerImplApi23(Context context, MediaSessionCompat.Token sessionToken)
- throws RemoteException {
- super(context, sessionToken);
- }
-
- @Override
- public TransportControls getTransportControls() {
- Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
- return controlsObj != null ? new TransportControlsApi23(controlsObj) : null;
- }
- }
-
- @RequiresApi(23)
- static class TransportControlsApi23 extends TransportControlsApi21 {
-
- public TransportControlsApi23(Object controlsObj) {
- super(controlsObj);
- }
-
- @Override
- public void playFromUri(Uri uri, Bundle extras) {
- MediaControllerCompatApi23.TransportControls.playFromUri(mControlsObj, uri,
- extras);
- }
- }
-
- @RequiresApi(24)
- static class MediaControllerImplApi24 extends MediaControllerImplApi23 {
-
- public MediaControllerImplApi24(Context context, MediaSessionCompat session) {
- super(context, session);
- }
-
- public MediaControllerImplApi24(Context context, MediaSessionCompat.Token sessionToken)
- throws RemoteException {
- super(context, sessionToken);
- }
-
- @Override
- public TransportControls getTransportControls() {
- Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
- return controlsObj != null ? new TransportControlsApi24(controlsObj) : null;
- }
- }
-
- @RequiresApi(24)
- static class TransportControlsApi24 extends TransportControlsApi23 {
-
- public TransportControlsApi24(Object controlsObj) {
- super(controlsObj);
- }
-
- @Override
- public void prepare() {
- MediaControllerCompatApi24.TransportControls.prepare(mControlsObj);
- }
-
- @Override
- public void prepareFromMediaId(String mediaId, Bundle extras) {
- MediaControllerCompatApi24.TransportControls.prepareFromMediaId(
- mControlsObj, mediaId, extras);
- }
-
- @Override
- public void prepareFromSearch(String query, Bundle extras) {
- MediaControllerCompatApi24.TransportControls.prepareFromSearch(
- mControlsObj, query, extras);
- }
-
- @Override
- public void prepareFromUri(Uri uri, Bundle extras) {
- MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
- }
- }
-}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/ClientBroadcastReceiver.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/ClientBroadcastReceiver.java
index 2ad4d86..3227482 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/ClientBroadcastReceiver.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/ClientBroadcastReceiver.java
@@ -63,12 +63,11 @@
import android.os.ResultReceiver;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.media.session.MediaControllerCompat;
-import androidx.media.session.MediaControllerCompat.TransportControls;
-
public class ClientBroadcastReceiver extends BroadcastReceiver {
@Override
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
index 3d181eb..227afad 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -71,14 +71,14 @@
import android.support.test.filters.MediumTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.MediaBrowserCompat.MediaItem;
import androidx.media.MediaBrowserServiceCompat;
import org.junit.After;
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
index b25812e..bc03f1a 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -75,18 +75,18 @@
import android.support.test.filters.LargeTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.ParcelableVolumeInfo;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
-import androidx.media.MediaBrowserCompat;
import androidx.media.VolumeProviderCompat;
-import androidx.media.session.MediaControllerCompat;
import org.junit.After;
import org.junit.Before;
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaItemTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaItemTest.java
index 52b93c1..179a178 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaItemTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/MediaItemTest.java
@@ -21,10 +21,9 @@
import android.os.Parcel;
import android.support.test.filters.SmallTest;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.MediaDescriptionCompat;
-import androidx.media.MediaBrowserCompat.MediaItem;
-
import org.junit.Test;
/**
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
index 2120015..3519f2f 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -90,13 +90,13 @@
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.KeyEvent;
import androidx.media.VolumeProviderCompat;
-import androidx.media.session.MediaControllerCompat;
import org.junit.After;
import org.junit.Before;
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
index f5045ec..d70ce8b 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
@@ -33,12 +33,12 @@
import static org.junit.Assert.assertNull;
import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
-import androidx.media.MediaBrowserCompat.MediaItem;
import androidx.media.MediaBrowserServiceCompat;
import java.util.ArrayList;
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
index 3e6f09d..7b31196 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
@@ -17,11 +17,11 @@
package android.support.mediacompat.service;
import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.media.MediaBrowserCompat;
import androidx.media.MediaBrowserServiceCompat;
import java.util.List;
diff --git a/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java b/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java
index 46a8f28..b7d9eb2 100644
--- a/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java
+++ b/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java
@@ -26,7 +26,7 @@
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.media.session.MediaSessionCompat;
-import androidx.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import org.junit.After;
import org.junit.Before;
diff --git a/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java b/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
index 717dbe9..c9fe73a 100644
--- a/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
+++ b/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
@@ -69,7 +69,7 @@
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.ObjectsCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
-import androidx.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import androidx.mediarouter.R;
import androidx.mediarouter.media.MediaRouteSelector;
import androidx.mediarouter.media.MediaRouter;
diff --git a/paging/common/build.gradle b/paging/common/build.gradle
index bc1d3a8..af21da4 100644
--- a/paging/common/build.gradle
+++ b/paging/common/build.gradle
@@ -26,7 +26,7 @@
dependencies {
compile(SUPPORT_ANNOTATIONS)
- compile(project(":arch:common"))
+ compile(project(":arch:core-common"))
testCompile(JUNIT)
testCompile(MOCKITO_CORE)
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java b/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
deleted file mode 100644
index b2e389f..0000000
--- a/paging/common/src/main/java/android/arch/paging/ContiguousDataSource.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.concurrent.Executor;
-
-abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
- @Override
- boolean isContiguous() {
- return true;
- }
-
- abstract void dispatchLoadInitial(
- @Nullable Key key,
- int initialLoadSize,
- int pageSize,
- boolean enablePlaceholders,
- @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver);
-
- abstract void dispatchLoadAfter(
- int currentEndIndex,
- @NonNull Value currentEndItem,
- int pageSize,
- @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver);
-
- abstract void dispatchLoadBefore(
- int currentBeginIndex,
- @NonNull Value currentBeginItem,
- int pageSize,
- @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver);
-
- /**
- * Get the key from either the position, or item, or null if position/item invalid.
- * <p>
- * Position may not match passed item's position - if trying to query the key from a position
- * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed.
- */
- abstract Key getKey(int position, Value item);
-}
diff --git a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java b/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
deleted file mode 100644
index b540ce1..0000000
--- a/paging/common/src/main/java/android/arch/paging/ContiguousPagedList.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.AnyThread;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback {
- private final ContiguousDataSource<K, V> mDataSource;
- private boolean mPrependWorkerRunning = false;
- private boolean mAppendWorkerRunning = false;
-
- private int mPrependItemsRequested = 0;
- private int mAppendItemsRequested = 0;
-
- private PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
- // Creation thread for initial synchronous load, otherwise main thread
- // Safe to access main thread only state - no other thread has reference during construction
- @AnyThread
- @Override
- public void onPageResult(@PageResult.ResultType int resultType,
- @NonNull PageResult<V> pageResult) {
- if (pageResult.isInvalid()) {
- detach();
- return;
- }
-
- if (isDetached()) {
- // No op, have detached
- return;
- }
-
- List<V> page = pageResult.page;
- if (resultType == PageResult.INIT) {
- mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
- pageResult.positionOffset, ContiguousPagedList.this);
- if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
- // Because the ContiguousPagedList wasn't initialized with a last load position,
- // initialize it to the middle of the initial load
- mLastLoad =
- pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
- }
- } else if (resultType == PageResult.APPEND) {
- mStorage.appendPage(page, ContiguousPagedList.this);
- } else if (resultType == PageResult.PREPEND) {
- mStorage.prependPage(page, ContiguousPagedList.this);
- } else {
- throw new IllegalArgumentException("unexpected resultType " + resultType);
- }
-
-
- if (mBoundaryCallback != null) {
- boolean deferEmpty = mStorage.size() == 0;
- boolean deferBegin = !deferEmpty
- && resultType == PageResult.PREPEND
- && pageResult.page.size() == 0;
- boolean deferEnd = !deferEmpty
- && resultType == PageResult.APPEND
- && pageResult.page.size() == 0;
- deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
- }
- }
- };
-
- static final int LAST_LOAD_UNSPECIFIED = -1;
-
- ContiguousPagedList(
- @NonNull ContiguousDataSource<K, V> dataSource,
- @NonNull Executor mainThreadExecutor,
- @NonNull Executor backgroundThreadExecutor,
- @Nullable BoundaryCallback<V> boundaryCallback,
- @NonNull Config config,
- final @Nullable K key,
- int lastLoad) {
- super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
- boundaryCallback, config);
- mDataSource = dataSource;
- mLastLoad = lastLoad;
-
- if (mDataSource.isInvalid()) {
- detach();
- } else {
- mDataSource.dispatchLoadInitial(key,
- mConfig.initialLoadSizeHint,
- mConfig.pageSize,
- mConfig.enablePlaceholders,
- mMainThreadExecutor,
- mReceiver);
- }
- }
-
- @MainThread
- @Override
- void dispatchUpdatesSinceSnapshot(
- @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) {
- final PagedStorage<V> snapshot = pagedListSnapshot.mStorage;
-
- final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended();
- final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended();
-
- final int previousTrailing = snapshot.getTrailingNullCount();
- final int previousLeading = snapshot.getLeadingNullCount();
-
- // Validate that the snapshot looks like a previous version of this list - if it's not,
- // we can't be sure we'll dispatch callbacks safely
- if (snapshot.isEmpty()
- || newlyAppended < 0
- || newlyPrepended < 0
- || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0)
- || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0)
- || (mStorage.getStorageCount()
- != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) {
- throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
- + " to be a snapshot of this PagedList");
- }
-
- if (newlyAppended != 0) {
- final int changedCount = Math.min(previousTrailing, newlyAppended);
- final int addedCount = newlyAppended - changedCount;
-
- final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount();
- if (changedCount != 0) {
- callback.onChanged(endPosition, changedCount);
- }
- if (addedCount != 0) {
- callback.onInserted(endPosition + changedCount, addedCount);
- }
- }
- if (newlyPrepended != 0) {
- final int changedCount = Math.min(previousLeading, newlyPrepended);
- final int addedCount = newlyPrepended - changedCount;
-
- if (changedCount != 0) {
- callback.onChanged(previousLeading, changedCount);
- }
- if (addedCount != 0) {
- callback.onInserted(0, addedCount);
- }
- }
- }
-
- @MainThread
- @Override
- protected void loadAroundInternal(int index) {
- int prependItems = mConfig.prefetchDistance - (index - mStorage.getLeadingNullCount());
- int appendItems = index + mConfig.prefetchDistance
- - (mStorage.getLeadingNullCount() + mStorage.getStorageCount());
-
- mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
- if (mPrependItemsRequested > 0) {
- schedulePrepend();
- }
-
- mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
- if (mAppendItemsRequested > 0) {
- scheduleAppend();
- }
- }
-
- @MainThread
- private void schedulePrepend() {
- if (mPrependWorkerRunning) {
- return;
- }
- mPrependWorkerRunning = true;
-
- final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset();
-
- // safe to access first item here - mStorage can't be empty if we're prepending
- final V item = mStorage.getFirstLoadedItem();
- mBackgroundThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (isDetached()) {
- return;
- }
- if (mDataSource.isInvalid()) {
- detach();
- } else {
- mDataSource.dispatchLoadBefore(position, item, mConfig.pageSize,
- mMainThreadExecutor, mReceiver);
- }
-
- }
- });
- }
-
- @MainThread
- private void scheduleAppend() {
- if (mAppendWorkerRunning) {
- return;
- }
- mAppendWorkerRunning = true;
-
- final int position = mStorage.getLeadingNullCount()
- + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
-
- // safe to access first item here - mStorage can't be empty if we're appending
- final V item = mStorage.getLastLoadedItem();
- mBackgroundThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (isDetached()) {
- return;
- }
- if (mDataSource.isInvalid()) {
- detach();
- } else {
- mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
- mMainThreadExecutor, mReceiver);
- }
- }
- });
- }
-
- @Override
- boolean isContiguous() {
- return true;
- }
-
- @NonNull
- @Override
- public DataSource<?, V> getDataSource() {
- return mDataSource;
- }
-
- @Nullable
- @Override
- public Object getLastKey() {
- return mDataSource.getKey(mLastLoad, mLastItem);
- }
-
- @MainThread
- @Override
- public void onInitialized(int count) {
- notifyInserted(0, count);
- }
-
- @MainThread
- @Override
- public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) {
- // consider whether to post more work, now that a page is fully prepended
- mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount;
- mPrependWorkerRunning = false;
- if (mPrependItemsRequested > 0) {
- // not done prepending, keep going
- schedulePrepend();
- }
-
- // finally dispatch callbacks, after prepend may have already been scheduled
- notifyChanged(leadingNulls, changedCount);
- notifyInserted(0, addedCount);
-
- offsetBoundaryAccessIndices(addedCount);
- }
-
- @MainThread
- @Override
- public void onPageAppended(int endPosition, int changedCount, int addedCount) {
- // consider whether to post more work, now that a page is fully appended
-
- mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
- mAppendWorkerRunning = false;
- if (mAppendItemsRequested > 0) {
- // not done appending, keep going
- scheduleAppend();
- }
-
- // finally dispatch callbacks, after append may have already been scheduled
- notifyChanged(endPosition, changedCount);
- notifyInserted(endPosition + changedCount, addedCount);
- }
-
- @MainThread
- @Override
- public void onPagePlaceholderInserted(int pageIndex) {
- throw new IllegalStateException("Tiled callback on ContiguousPagedList");
- }
-
- @MainThread
- @Override
- public void onPageInserted(int start, int count) {
- throw new IllegalStateException("Tiled callback on ContiguousPagedList");
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/DataSource.java b/paging/common/src/main/java/android/arch/paging/DataSource.java
deleted file mode 100644
index 91360b0..0000000
--- a/paging/common/src/main/java/android/arch/paging/DataSource.java
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.AnyThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Base class for loading pages of snapshot data into a {@link PagedList}.
- * <p>
- * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
- * it loads more data, but the data loaded cannot be updated. If the underlying data set is
- * modified, a new PagedList / DataSource pair must be created to represent the new data.
- * <h4>Loading Pages</h4>
- * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
- * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
- * <p>
- * To control how and when a PagedList queries data from its DataSource, see
- * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
- * <h4>Updating Paged Data</h4>
- * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
- * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
- * content update occurs. A DataSource must detect that it cannot continue loading its
- * snapshot (for instance, when Database query notices a table being invalidated), and call
- * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
- * the new state of the Database query.
- * <p>
- * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
- * PagedList. For example, loading from network when the network's paging API doesn't provide
- * updates.
- * <p>
- * To page in data from a source that does provide updates, you can create a
- * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
- * data set occurs that makes the current snapshot invalid. For example, when paging a query from
- * the Database, and the table being queried inserts or removes items. You can also use a
- * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
- * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
- * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
- * DataSource.
- * <p>
- * If you have more granular update signals, such as a network API signaling an update to a single
- * item in the list, it's recommended to load data from network into memory. Then present that
- * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
- * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
- * snapshot can be created.
- * <h4>Implementing a DataSource</h4>
- * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
- * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
- * <p>
- * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
- * example a network response that returns some items, and a next/previous page links.
- * <p>
- * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
- * {@code N}. For example, if requesting the backend for the next comments in the list
- * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
- * from a name-sorted database query requires the name and unique ID of the previous.
- * <p>
- * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
- * PositionalDataSource is required to respect page size for efficient tiling. If you want to
- * override page size (e.g. when network page size constraints are only known at runtime), use one
- * of the other DataSource classes.
- * <p>
- * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
- * return {@code null} items in lists that it loads. This is so that users of the PagedList
- * can differentiate unloaded placeholder items from content that has been paged in.
- *
- * @param <Key> Input used to trigger initial load from the DataSource. Often an Integer position.
- * @param <Value> Value type loaded by the DataSource.
- */
-@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
-public abstract class DataSource<Key, Value> {
- /**
- * Factory for DataSources.
- * <p>
- * Data-loading systems of an application or library can implement this interface to allow
- * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
- * DataSource.Factory for a given SQL query:
- *
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- * public abstract DataSource.Factory<Integer, User> usersByLastName();
- * }
- * </pre>
- * In the above sample, {@code Integer} is used because it is the {@code Key} type of
- * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
- * page a large query with a PositionalDataSource.
- *
- * @param <Key> Key identifying items in DataSource.
- * @param <Value> Type of items in the list loaded by the DataSources.
- */
- public abstract static class Factory<Key, Value> {
- /**
- * Create a DataSource.
- * <p>
- * The DataSource should invalidate itself if the snapshot is no longer valid. If a
- * DataSource becomes invalid, the only way to query more data is to create a new DataSource
- * from the Factory.
- * <p>
- * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
- * when the current DataSource is invalidated, and pass the new PagedList through the
- * {@code LiveData<PagedList>} to observers.
- *
- * @return the new DataSource.
- */
- public abstract DataSource<Key, Value> create();
-
- /**
- * Applies the given function to each value emitted by DataSources produced by this Factory.
- * <p>
- * Same as {@link #mapByPage(Function)}, but operates on individual items.
- *
- * @param function Function that runs on each loaded item, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource.Factory, which transforms items using the given function.
- *
- * @see #mapByPage(Function)
- * @see DataSource#map(Function)
- * @see DataSource#mapByPage(Function)
- */
- @NonNull
- public <ToValue> DataSource.Factory<Key, ToValue> map(
- @NonNull Function<Value, ToValue> function) {
- return mapByPage(createListFunction(function));
- }
-
- /**
- * Applies the given function to each value emitted by DataSources produced by this Factory.
- * <p>
- * Same as {@link #map(Function)}, but allows for batch conversions.
- *
- * @param function Function that runs on each loaded page, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource.Factory, which transforms items using the given function.
- *
- * @see #map(Function)
- * @see DataSource#map(Function)
- * @see DataSource#mapByPage(Function)
- */
- @NonNull
- public <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
- @NonNull final Function<List<Value>, List<ToValue>> function) {
- return new Factory<Key, ToValue>() {
- @Override
- public DataSource<Key, ToValue> create() {
- return Factory.this.create().mapByPage(function);
- }
- };
- }
- }
-
- @NonNull
- static <X, Y> Function<List<X>, List<Y>> createListFunction(
- final @NonNull Function<X, Y> innerFunc) {
- return new Function<List<X>, List<Y>>() {
- @Override
- public List<Y> apply(@NonNull List<X> source) {
- List<Y> out = new ArrayList<>(source.size());
- for (int i = 0; i < source.size(); i++) {
- out.add(innerFunc.apply(source.get(i)));
- }
- return out;
- }
- };
- }
-
- static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
- List<B> dest = function.apply(source);
- if (dest.size() != source.size()) {
- throw new IllegalStateException("Invalid Function " + function
- + " changed return size. This is not supported.");
- }
- return dest;
- }
-
- // Since we currently rely on implementation details of two implementations,
- // prevent external subclassing, except through exposed subclasses
- DataSource() {
- }
-
- /**
- * Applies the given function to each value emitted by the DataSource.
- * <p>
- * Same as {@link #map(Function)}, but allows for batch conversions.
- *
- * @param function Function that runs on each loaded page, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource, which transforms items using the given function.
- *
- * @see #map(Function)
- * @see DataSource.Factory#map(Function)
- * @see DataSource.Factory#mapByPage(Function)
- */
- @NonNull
- public abstract <ToValue> DataSource<Key, ToValue> mapByPage(
- @NonNull Function<List<Value>, List<ToValue>> function);
-
- /**
- * Applies the given function to each value emitted by the DataSource.
- * <p>
- * Same as {@link #mapByPage(Function)}, but operates on individual items.
- *
- * @param function Function that runs on each loaded item, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource, which transforms items using the given function.
- *
- * @see #mapByPage(Function)
- * @see DataSource.Factory#map(Function)
- * @see DataSource.Factory#mapByPage(Function)
- */
- @NonNull
- public abstract <ToValue> DataSource<Key, ToValue> map(
- @NonNull Function<Value, ToValue> function);
-
- /**
- * Returns true if the data source guaranteed to produce a contiguous set of items,
- * never producing gaps.
- */
- abstract boolean isContiguous();
-
- static class LoadCallbackHelper<T> {
- static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative");
- }
- if (data.size() + position > totalCount) {
- throw new IllegalArgumentException(
- "List size + position too large, last item in list beyond totalCount.");
- }
- if (data.size() == 0 && totalCount > 0) {
- throw new IllegalArgumentException(
- "Initial result cannot be empty if items are present in data set.");
- }
- }
-
- @PageResult.ResultType
- final int mResultType;
- private final DataSource mDataSource;
- private final PageResult.Receiver<T> mReceiver;
-
- // mSignalLock protects mPostExecutor, and mHasSignalled
- private final Object mSignalLock = new Object();
- private Executor mPostExecutor = null;
- private boolean mHasSignalled = false;
-
- LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
- @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- mDataSource = dataSource;
- mResultType = resultType;
- mPostExecutor = mainThreadExecutor;
- mReceiver = receiver;
- }
-
- void setPostExecutor(Executor postExecutor) {
- synchronized (mSignalLock) {
- mPostExecutor = postExecutor;
- }
- }
-
- /**
- * Call before verifying args, or dispatching actul results
- *
- * @return true if DataSource was invalid, and invalid result dispatched
- */
- boolean dispatchInvalidResultIfInvalid() {
- if (mDataSource.isInvalid()) {
- dispatchResultToReceiver(PageResult.<T>getInvalidResult());
- return true;
- }
- return false;
- }
-
- void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
- Executor executor;
- synchronized (mSignalLock) {
- if (mHasSignalled) {
- throw new IllegalStateException(
- "callback.onResult already called, cannot call again.");
- }
- mHasSignalled = true;
- executor = mPostExecutor;
- }
-
- if (executor != null) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- mReceiver.onPageResult(mResultType, result);
- }
- });
- } else {
- mReceiver.onPageResult(mResultType, result);
- }
- }
- }
-
- /**
- * Invalidation callback for DataSource.
- * <p>
- * Used to signal when a DataSource a data source has become invalid, and that a new data source
- * is needed to continue loading data.
- */
- public interface InvalidatedCallback {
- /**
- * Called when the data backing the list has become invalid. This callback is typically used
- * to signal that a new data source is needed.
- * <p>
- * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
- * for the data source to invalidate itself during its load methods, or for an outside
- * source to invalidate it.
- */
- @AnyThread
- void onInvalidated();
- }
-
- private AtomicBoolean mInvalid = new AtomicBoolean(false);
-
- private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
- new CopyOnWriteArrayList<>();
-
- /**
- * Add a callback to invoke when the DataSource is first invalidated.
- * <p>
- * Once invalidated, a data source will not become valid again.
- * <p>
- * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
- * is called, on that thread.
- *
- * @param onInvalidatedCallback The callback, will be invoked on thread that
- * {@link #invalidate()} is called on.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.add(onInvalidatedCallback);
- }
-
- /**
- * Remove a previously added invalidate callback.
- *
- * @param onInvalidatedCallback The previously added callback.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
- }
-
- /**
- * Signal the data source to stop loading, and notify its callback.
- * <p>
- * If invalidate has already been called, this method does nothing.
- */
- @AnyThread
- public void invalidate() {
- if (mInvalid.compareAndSet(false, true)) {
- for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
- callback.onInvalidated();
- }
- }
- }
-
- /**
- * Returns true if the data source is invalid, and can no longer be queried for data.
- *
- * @return True if the data source is invalid, and can no longer return data.
- */
- @WorkerThread
- public boolean isInvalid() {
- return mInvalid.get();
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
deleted file mode 100644
index f68fae1..0000000
--- a/paging/common/src/main/java/android/arch/paging/ItemKeyedDataSource.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * Copyright 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Incremental data loader for paging keyed content, where loaded content uses previously loaded
- * items as input to future loads.
- * <p>
- * Implement a DataSource using ItemKeyedDataSource if you need to use data from item {@code N - 1}
- * to load item {@code N}. This is common, for example, in sorted database queries where
- * attributes of the item such just before the next query define how to execute it.
- * <p>
- * The {@code InMemoryByItemRepository} in the
- * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
- * shows how to implement a network ItemKeyedDataSource using
- * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
- * handling swipe-to-refresh, network errors, and retry.
- *
- * @param <Key> Type of data used to query Value types out of the DataSource.
- * @param <Value> Type of items being loaded by the DataSource.
- */
-public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
-
- /**
- * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
- *
- * @param <Key> Type of data used to query Value types out of the DataSource.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadInitialParams<Key> {
- /**
- * Load items around this key, or at the beginning of the data set if {@code null} is
- * passed.
- * <p>
- * Note that this key is generally a hint, and may be ignored if you want to always load
- * from the beginning.
- */
- @Nullable
- public final Key requestedInitialKey;
-
- /**
- * Requested number of items to load.
- * <p>
- * Note that this may be larger than available data.
- */
- public final int requestedLoadSize;
-
- /**
- * Defines whether placeholders are enabled, and whether the total count passed to
- * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
- */
- public final boolean placeholdersEnabled;
-
-
- public LoadInitialParams(@Nullable Key requestedInitialKey, int requestedLoadSize,
- boolean placeholdersEnabled) {
- this.requestedInitialKey = requestedInitialKey;
- this.requestedLoadSize = requestedLoadSize;
- this.placeholdersEnabled = placeholdersEnabled;
- }
- }
-
- /**
- * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)}
- * and {@link #loadAfter(LoadParams, LoadCallback)}.
- *
- * @param <Key> Type of data used to query Value types out of the DataSource.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadParams<Key> {
- /**
- * Load items before/after this key.
- * <p>
- * Returned data must begin directly adjacent to this position.
- */
- public final Key key;
- /**
- * Requested number of items to load.
- * <p>
- * Returned page can be of this size, but it may be altered if that is easier, e.g. a
- * network data source where the backend defines page size.
- */
- public final int requestedLoadSize;
-
- public LoadParams(Key key, int requestedLoadSize) {
- this.key = key;
- this.requestedLoadSize = requestedLoadSize;
- }
- }
-
- /**
- * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
- * to return data and, optionally, position/count information.
- * <p>
- * A callback can be called only once, and will throw if called again.
- * <p>
- * If you can compute the number of items in the data set before and after the loaded range,
- * call the three parameter {@link #onResult(List, int, int)} to pass that information. You
- * can skip passing this information by calling the single parameter {@link #onResult(List)},
- * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is
- * {@code false}, so the positioning information will be ignored.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <Value> Type of items being loaded.
- */
- public abstract static class LoadInitialCallback<Value> extends LoadCallback<Value> {
- /**
- * Called to pass initial load state from a DataSource.
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * and inform how many placeholders should be shown before and after. If counting is cheap
- * to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass data back through this method.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be loaded from this DataSource,
- * pass {@code N}.
- * @param totalCount Total number of items that may be returned from this DataSource.
- * Includes the number in the initial {@code data} parameter
- * as well as any items that can be loaded in front or behind of
- * {@code data}.
- */
- public abstract void onResult(@NonNull List<Value> data, int position, int totalCount);
- }
-
-
- /**
- * Callback for ItemKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)}
- * and {@link #loadAfter(LoadParams, LoadCallback)} to return data.
- * <p>
- * A callback can be called only once, and will throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <Value> Type of items being loaded.
- */
- public abstract static class LoadCallback<Value> {
- /**
- * Called to pass loaded data from a DataSource.
- * <p>
- * Call this method from your ItemKeyedDataSource's
- * {@link #loadBefore(LoadParams, LoadCallback)} and
- * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
- * <p>
- * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
- * initialize without counting available data, or supporting placeholders.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- *
- * @param data List of items loaded from the ItemKeyedDataSource.
- */
- public abstract void onResult(@NonNull List<Value> data);
- }
-
- static class LoadInitialCallbackImpl<Value> extends LoadInitialCallback<Value> {
- final LoadCallbackHelper<Value> mCallbackHelper;
- private final boolean mCountingEnabled;
- LoadInitialCallbackImpl(@NonNull ItemKeyedDataSource dataSource, boolean countingEnabled,
- @NonNull PageResult.Receiver<Value> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
- mCountingEnabled = countingEnabled;
- }
-
- @Override
- public void onResult(@NonNull List<Value> data, int position, int totalCount) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
-
- int trailingUnloadedCount = totalCount - position - data.size();
- if (mCountingEnabled) {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
- data, position, trailingUnloadedCount, 0));
- } else {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- @Override
- public void onResult(@NonNull List<Value> data) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
- }
- }
- }
-
- static class LoadCallbackImpl<Value> extends LoadCallback<Value> {
- final LoadCallbackHelper<Value> mCallbackHelper;
-
- LoadCallbackImpl(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type,
- @Nullable Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(
- dataSource, type, mainThreadExecutor, receiver);
- }
-
- @Override
- public void onResult(@NonNull List<Value> data) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
- }
- }
- }
-
- @Nullable
- @Override
- final Key getKey(int position, Value item) {
- if (item == null) {
- return null;
- }
-
- return getKey(item);
- }
-
- @Override
- final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
- boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- LoadInitialCallbackImpl<Value> callback =
- new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
- loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
-
- // If initialLoad's callback is not called within the body, we force any following calls
- // to post to the UI thread. This constructor may be run on a background thread, but
- // after constructor, mutation must happen on UI thread.
- callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
- }
-
- @Override
- final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
- int pageSize, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),
- new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
- }
-
- @Override
- final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
- int pageSize, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),
- new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
- }
-
- /**
- * Load initial data.
- * <p>
- * This method is called first to initialize a PagedList with data. If it's possible to count
- * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
- * the callback via the three-parameter
- * {@link LoadInitialCallback#onResult(List, int, int)}. This enables PagedLists
- * presenting data from this source to display placeholders to represent unloaded items.
- * <p>
- * {@link LoadInitialParams#requestedInitialKey} and {@link LoadInitialParams#requestedLoadSize}
- * are hints, not requirements, so they may be altered or ignored. Note that ignoring the
- * {@code requestedInitialKey} can prevent subsequent PagedList/DataSource pairs from
- * initializing at the same location. If your data source never invalidates (for example,
- * loading from the network without the network ever signalling that old data must be reloaded),
- * it's fine to ignore the {@code initialLoadKey} and always start from the beginning of the
- * data set.
- *
- * @param params Parameters for initial load, including initial key and requested size.
- * @param callback Callback that receives initial load data.
- */
- public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
- @NonNull LoadInitialCallback<Value> callback);
-
- /**
- * Load list data after the key specified in {@link LoadParams#key LoadParams.key}.
- * <p>
- * It's valid to return a different list size than the page size if it's easier, e.g. if your
- * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
- * <p>
- * Data may be passed synchronously during the loadAfter method, or deferred and called at a
- * later time. Further loads going down will be blocked until the callback is called.
- * <p>
- * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
- * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
- * and prevent further loading.
- *
- * @param params Parameters for the load, including the key to load after, and requested size.
- * @param callback Callback that receives loaded data.
- */
- public abstract void loadAfter(@NonNull LoadParams<Key> params,
- @NonNull LoadCallback<Value> callback);
-
- /**
- * Load list data before the key specified in {@link LoadParams#key LoadParams.key}.
- * <p>
- * It's valid to return a different list size than the page size if it's easier, e.g. if your
- * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
- * <p>
- * <p class="note"><strong>Note:</strong> Data returned will be prepended just before the key
- * passed, so if you vary size, ensure that the last item is adjacent to the passed key.
- * <p>
- * Data may be passed synchronously during the loadBefore method, or deferred and called at a
- * later time. Further loads going up will be blocked until the callback is called.
- * <p>
- * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
- * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
- * and prevent further loading.
- *
- * @param params Parameters for the load, including the key to load before, and requested size.
- * @param callback Callback that receives loaded data.
- */
- public abstract void loadBefore(@NonNull LoadParams<Key> params,
- @NonNull LoadCallback<Value> callback);
-
- /**
- * Return a key associated with the given item.
- * <p>
- * If your ItemKeyedDataSource 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(LoadParams, LoadCallback)} or
- * {@link #loadAfter(LoadParams, LoadCallback)} 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
- public abstract Key getKey(@NonNull Value item);
-
- @NonNull
- @Override
- public final <ToValue> ItemKeyedDataSource<Key, ToValue> mapByPage(
- @NonNull Function<List<Value>, List<ToValue>> function) {
- return new WrapperItemKeyedDataSource<>(this, function);
- }
-
- @NonNull
- @Override
- public final <ToValue> ItemKeyedDataSource<Key, ToValue> map(
- @NonNull Function<Value, ToValue> function) {
- return mapByPage(createListFunction(function));
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/ListDataSource.java b/paging/common/src/main/java/android/arch/paging/ListDataSource.java
deleted file mode 100644
index 1482a91..0000000
--- a/paging/common/src/main/java/android/arch/paging/ListDataSource.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.List;
-
-class ListDataSource<T> extends PositionalDataSource<T> {
- private final List<T> mList;
-
- public ListDataSource(List<T> list) {
- mList = new ArrayList<>(list);
- }
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams params,
- @NonNull LoadInitialCallback<T> callback) {
- final int totalCount = mList.size();
-
- final int position = computeInitialLoadPosition(params, totalCount);
- final int loadSize = computeInitialLoadSize(params, position, totalCount);
-
- // for simplicity, we could return everything immediately,
- // but we tile here since it's expected behavior
- List<T> sublist = mList.subList(position, position + loadSize);
- callback.onResult(sublist, position, totalCount);
- }
-
- @Override
- public void loadRange(@NonNull LoadRangeParams params,
- @NonNull LoadRangeCallback<T> callback) {
- callback.onResult(mList.subList(params.startPosition,
- params.startPosition + params.loadSize));
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
deleted file mode 100644
index 7fc5166..0000000
--- a/paging/common/src/main/java/android/arch/paging/PageKeyedDataSource.java
+++ /dev/null
@@ -1,425 +0,0 @@
-/*
- * Copyright 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.GuardedBy;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Incremental data loader for page-keyed content, where requests return keys for next/previous
- * pages.
- * <p>
- * Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1}
- * to load page {@code N}. This is common, for example, in network APIs that include a next/previous
- * link or key with each page load.
- * <p>
- * The {@code InMemoryByPageRepository} in the
- * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
- * shows how to implement a network PageKeyedDataSource using
- * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
- * handling swipe-to-refresh, network errors, and retry.
- *
- * @param <Key> Type of data used to query Value types out of the DataSource.
- * @param <Value> Type of items being loaded by the DataSource.
- */
-public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
- private final Object mKeyLock = new Object();
-
- @Nullable
- @GuardedBy("mKeyLock")
- private Key mNextKey = null;
-
- @Nullable
- @GuardedBy("mKeyLock")
- private Key mPreviousKey = null;
-
- private void initKeys(@Nullable Key previousKey, @Nullable Key nextKey) {
- synchronized (mKeyLock) {
- mPreviousKey = previousKey;
- mNextKey = nextKey;
- }
- }
-
- private void setPreviousKey(@Nullable Key previousKey) {
- synchronized (mKeyLock) {
- mPreviousKey = previousKey;
- }
- }
-
- private void setNextKey(@Nullable Key nextKey) {
- synchronized (mKeyLock) {
- mNextKey = nextKey;
- }
- }
-
- private @Nullable Key getPreviousKey() {
- synchronized (mKeyLock) {
- return mPreviousKey;
- }
- }
-
- private @Nullable Key getNextKey() {
- synchronized (mKeyLock) {
- return mNextKey;
- }
- }
-
- /**
- * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
- *
- * @param <Key> Type of data used to query pages.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadInitialParams<Key> {
- /**
- * Requested number of items to load.
- * <p>
- * Note that this may be larger than available data.
- */
- public final int requestedLoadSize;
-
- /**
- * Defines whether placeholders are enabled, and whether the total count passed to
- * {@link LoadInitialCallback#onResult(List, int, int, Key, Key)} will be ignored.
- */
- public final boolean placeholdersEnabled;
-
-
- public LoadInitialParams(int requestedLoadSize, boolean placeholdersEnabled) {
- this.requestedLoadSize = requestedLoadSize;
- this.placeholdersEnabled = placeholdersEnabled;
- }
- }
-
- /**
- * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} and
- * {@link #loadAfter(LoadParams, LoadCallback)}.
- *
- * @param <Key> Type of data used to query pages.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadParams<Key> {
- /**
- * Load items before/after this key.
- * <p>
- * Returned data must begin directly adjacent to this position.
- */
- public final Key key;
-
- /**
- * Requested number of items to load.
- * <p>
- * Returned page can be of this size, but it may be altered if that is easier, e.g. a
- * network data source where the backend defines page size.
- */
- public final int requestedLoadSize;
-
- public LoadParams(Key key, int requestedLoadSize) {
- this.key = key;
- this.requestedLoadSize = requestedLoadSize;
- }
- }
-
- /**
- * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
- * to return data and, optionally, position/count information.
- * <p>
- * A callback can be called only once, and will throw if called again.
- * <p>
- * If you can compute the number of items in the data set before and after the loaded range,
- * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that
- * information. You can skip passing this information by calling the three parameter
- * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if
- * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning
- * information will be ignored.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <Key> Type of data used to query pages.
- * @param <Value> Type of items being loaded.
- */
- public abstract static class LoadInitialCallback<Key, Value> {
- /**
- * Called to pass initial load state from a DataSource.
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * and inform how many placeholders should be shown before and after. If counting is cheap
- * to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass data back through this method.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be loaded from this DataSource,
- * pass {@code N}.
- * @param totalCount Total number of items that may be returned from this DataSource.
- * Includes the number in the initial {@code data} parameter
- * as well as any items that can be loaded in front or behind of
- * {@code data}.
- */
- public abstract void onResult(@NonNull List<Value> data, int position, int totalCount,
- @Nullable Key previousPageKey, @Nullable Key nextPageKey);
-
- /**
- * Called to pass loaded data from a DataSource.
- * <p>
- * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
- * initialize without counting available data, or supporting placeholders.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- *
- * @param data List of items loaded from the PageKeyedDataSource.
- * @param previousPageKey Key for page before the initial load result, or {@code null} if no
- * more data can be loaded before.
- * @param nextPageKey Key for page after the initial load result, or {@code null} if no
- * more data can be loaded after.
- */
- public abstract void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
- @Nullable Key nextPageKey);
- }
-
- /**
- * Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and
- * {@link #loadAfter(LoadParams, LoadCallback)} to return data.
- * <p>
- * A callback can be called only once, and will throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <Key> Type of data used to query pages.
- * @param <Value> Type of items being loaded.
- */
- public abstract static class LoadCallback<Key, Value> {
-
- /**
- * Called to pass loaded data from a DataSource.
- * <p>
- * Call this method from your PageKeyedDataSource's
- * {@link #loadBefore(LoadParams, LoadCallback)} and
- * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- * <p>
- * Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've
- * loaded a page in {@link #loadBefore(LoadParams, LoadCallback)}, pass the key for the
- * previous page, or {@code null} if the loaded page is the first. If in
- * {@link #loadAfter(LoadParams, LoadCallback)}, pass the key for the next page, or
- * {@code null} if the loaded page is the last.
- *
- * @param data List of items loaded from the PageKeyedDataSource.
- * @param adjacentPageKey Key for subsequent page load (previous page in {@link #loadBefore}
- * / next page in {@link #loadAfter}), or {@code null} if there are
- * no more pages to load in the current load direction.
- */
- public abstract void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey);
- }
-
- static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {
- final LoadCallbackHelper<Value> mCallbackHelper;
- private final PageKeyedDataSource<Key, Value> mDataSource;
- private final boolean mCountingEnabled;
- LoadInitialCallbackImpl(@NonNull PageKeyedDataSource<Key, Value> dataSource,
- boolean countingEnabled, @NonNull PageResult.Receiver<Value> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(
- dataSource, PageResult.INIT, null, receiver);
- mDataSource = dataSource;
- mCountingEnabled = countingEnabled;
- }
-
- @Override
- public void onResult(@NonNull List<Value> data, int position, int totalCount,
- @Nullable Key previousPageKey, @Nullable Key nextPageKey) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
-
- // setup keys before dispatching data, so guaranteed to be ready
- mDataSource.initKeys(previousPageKey, nextPageKey);
-
- int trailingUnloadedCount = totalCount - position - data.size();
- if (mCountingEnabled) {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
- data, position, trailingUnloadedCount, 0));
- } else {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- @Override
- public void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
- @Nullable Key nextPageKey) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- mDataSource.initKeys(previousPageKey, nextPageKey);
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
- }
- }
- }
-
- static class LoadCallbackImpl<Key, Value> extends LoadCallback<Key, Value> {
- final LoadCallbackHelper<Value> mCallbackHelper;
- private final PageKeyedDataSource<Key, Value> mDataSource;
- LoadCallbackImpl(@NonNull PageKeyedDataSource<Key, Value> dataSource,
- @PageResult.ResultType int type, @Nullable Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(
- dataSource, type, mainThreadExecutor, receiver);
- mDataSource = dataSource;
- }
-
- @Override
- public void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- if (mCallbackHelper.mResultType == PageResult.APPEND) {
- mDataSource.setNextKey(adjacentPageKey);
- } else {
- mDataSource.setPreviousKey(adjacentPageKey);
- }
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
- }
- }
- }
-
- @Nullable
- @Override
- final Key getKey(int position, Value item) {
- // don't attempt to persist keys, since we currently don't pass them to initial load
- return null;
- }
-
- @Override
- final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
- boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- LoadInitialCallbackImpl<Key, Value> callback =
- new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
- loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
-
- // If initialLoad's callback is not called within the body, we force any following calls
- // to post to the UI thread. This constructor may be run on a background thread, but
- // after constructor, mutation must happen on UI thread.
- callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
- }
-
-
- @Override
- final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
- int pageSize, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- @Nullable Key key = getNextKey();
- if (key != null) {
- loadAfter(new LoadParams<>(key, pageSize),
- new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
- }
- }
-
- @Override
- final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
- int pageSize, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- @Nullable Key key = getPreviousKey();
- if (key != null) {
- loadBefore(new LoadParams<>(key, pageSize),
- new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
- }
- }
-
- /**
- * Load initial data.
- * <p>
- * This method is called first to initialize a PagedList with data. If it's possible to count
- * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
- * the callback via the three-parameter
- * {@link LoadInitialCallback#onResult(List, int, int, Object, Object)}. This enables PagedLists
- * presenting data from this source to display placeholders to represent unloaded items.
- * <p>
- * {@link LoadInitialParams#requestedLoadSize} is a hint, not a requirement, so it may be may be
- * altered or ignored.
- *
- * @param params Parameters for initial load, including requested load size.
- * @param callback Callback that receives initial load data.
- */
- public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
- @NonNull LoadInitialCallback<Key, Value> callback);
-
- /**
- * Prepend page with the key specified by {@link LoadParams#key LoadParams.key}.
- * <p>
- * It's valid to return a different list size than the page size if it's easier, e.g. if your
- * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
- * <p>
- * Data may be passed synchronously during the load method, or deferred and called at a
- * later time. Further loads going down will be blocked until the callback is called.
- * <p>
- * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
- * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
- * and prevent further loading.
- *
- * @param params Parameters for the load, including the key for the new page, and requested load
- * size.
- * @param callback Callback that receives loaded data.
- */
- public abstract void loadBefore(@NonNull LoadParams<Key> params,
- @NonNull LoadCallback<Key, Value> callback);
-
- /**
- * Append page with the key specified by {@link LoadParams#key LoadParams.key}.
- * <p>
- * It's valid to return a different list size than the page size if it's easier, e.g. if your
- * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
- * <p>
- * Data may be passed synchronously during the load method, or deferred and called at a
- * later time. Further loads going down will be blocked until the callback is called.
- * <p>
- * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
- * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
- * and prevent further loading.
- *
- * @param params Parameters for the load, including the key for the new page, and requested load
- * size.
- * @param callback Callback that receives loaded data.
- */
- public abstract void loadAfter(@NonNull LoadParams<Key> params,
- @NonNull LoadCallback<Key, Value> callback);
-
- @NonNull
- @Override
- public final <ToValue> PageKeyedDataSource<Key, ToValue> mapByPage(
- @NonNull Function<List<Value>, List<ToValue>> function) {
- return new WrapperPageKeyedDataSource<>(this, function);
- }
-
- @NonNull
- @Override
- public final <ToValue> PageKeyedDataSource<Key, ToValue> map(
- @NonNull Function<Value, ToValue> function) {
- return mapByPage(createListFunction(function));
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PageResult.java b/paging/common/src/main/java/android/arch/paging/PageResult.java
deleted file mode 100644
index cf2216f..0000000
--- a/paging/common/src/main/java/android/arch/paging/PageResult.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.paging;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.support.annotation.IntDef;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-
-import java.lang.annotation.Retention;
-import java.util.Collections;
-import java.util.List;
-
-class PageResult<T> {
- @SuppressWarnings("unchecked")
- private static final PageResult INVALID_RESULT =
- new PageResult(Collections.EMPTY_LIST, 0);
-
- @SuppressWarnings("unchecked")
- static <T> PageResult<T> getInvalidResult() {
- return INVALID_RESULT;
- }
-
-
- @Retention(SOURCE)
- @IntDef({INIT, APPEND, PREPEND, TILE})
- @interface ResultType {}
-
- static final int INIT = 0;
-
- // contiguous results
- static final int APPEND = 1;
- static final int PREPEND = 2;
-
- // non-contiguous, tile result
- static final int TILE = 3;
-
- @NonNull
- public final List<T> page;
- @SuppressWarnings("WeakerAccess")
- public final int leadingNulls;
- @SuppressWarnings("WeakerAccess")
- public final int trailingNulls;
- @SuppressWarnings("WeakerAccess")
- public final int positionOffset;
-
- PageResult(@NonNull List<T> list, int leadingNulls, int trailingNulls, int positionOffset) {
- this.page = list;
- this.leadingNulls = leadingNulls;
- this.trailingNulls = trailingNulls;
- this.positionOffset = positionOffset;
- }
-
- PageResult(@NonNull List<T> list, int positionOffset) {
- this.page = list;
- this.leadingNulls = 0;
- this.trailingNulls = 0;
- this.positionOffset = positionOffset;
- }
-
- @Override
- public String toString() {
- return "Result " + leadingNulls
- + ", " + page
- + ", " + trailingNulls
- + ", offset " + positionOffset;
- }
-
- public boolean isInvalid() {
- return this == INVALID_RESULT;
- }
-
- abstract static class Receiver<T> {
- @MainThread
- public abstract void onPageResult(@ResultType int type, @NonNull PageResult<T> pageResult);
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PagedList.java b/paging/common/src/main/java/android/arch/paging/PagedList.java
deleted file mode 100644
index a2ea3ab..0000000
--- a/paging/common/src/main/java/android/arch/paging/PagedList.java
+++ /dev/null
@@ -1,1005 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.AnyThread;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.WorkerThread;
-
-import java.lang.ref.WeakReference;
-import java.util.AbstractList;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Lazy loading list that pages in immutable content from a {@link DataSource}.
- * <p>
- * 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)}. To display a PagedList, see {@link PagedListAdapter}, which enables the
- * binding of a PagedList to a {@link android.support.v7.widget.RecyclerView}.
- * <h4>Loading Data</h4>
- * <p>
- * All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads the
- * first chunk of 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>
- * A PagedList initially presents this first partial load as its content, and expands over time as
- * content is loaded in. 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 LivePagedListBuilder} 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 does not count its data set in its initial load, or if {@code false} is passed to
- * {@link Config.Builder#setEnablePlaceholders(boolean)} when building a {@link Config}.
- * <h4>Mutability and Snapshots</h4>
- * A PagedList is <em>mutable</em> while loading, or ready to load from its DataSource.
- * As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can
- * listen to these updates with a {@link Callback}. (Note that {@link PagedListAdapter} will listen
- * to these to signal RecyclerView about the updates/changes).
- * <p>
- * If a PagedList attempts to load from an invalid DataSource, it will {@link #detach()}
- * from the DataSource, meaning that it will no longer attempt to load data. It will return true
- * from {@link #isImmutable()}, and a new DataSource / PagedList pair must be created to load
- * further data. See {@link DataSource} and {@link LivePagedListBuilder} for how new PagedLists are
- * created to represent changed data.
- * <p>
- * A PagedList snapshot is simply an immutable shallow copy of the current state of the PagedList as
- * a {@code List}. It will reference the same inner items, and contain the same {@code null}
- * placeholders, if present.
- *
- * @param <T> The type of the entries in the list.
- */
-public abstract class PagedList<T> extends AbstractList<T> {
- @NonNull
- final Executor mMainThreadExecutor;
- @NonNull
- final Executor mBackgroundThreadExecutor;
- @Nullable
- final BoundaryCallback<T> mBoundaryCallback;
- @NonNull
- final Config mConfig;
- @NonNull
- final PagedStorage<T> mStorage;
-
- int mLastLoad = 0;
- T mLastItem = null;
-
- // if set to true, mBoundaryCallback is non-null, and should
- // be dispatched when nearby load has occurred
- private boolean mBoundaryCallbackBeginDeferred = false;
- private boolean mBoundaryCallbackEndDeferred = false;
-
- // lowest and highest index accessed by loadAround. Used to
- // decide when mBoundaryCallback should be dispatched
- private int mLowestIndexAccessed = Integer.MAX_VALUE;
- private int mHighestIndexAccessed = Integer.MIN_VALUE;
-
- private final AtomicBoolean mDetached = new AtomicBoolean(false);
-
- protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
-
- PagedList(@NonNull PagedStorage<T> storage,
- @NonNull Executor mainThreadExecutor,
- @NonNull Executor backgroundThreadExecutor,
- @Nullable BoundaryCallback<T> boundaryCallback,
- @NonNull Config config) {
- mStorage = storage;
- mMainThreadExecutor = mainThreadExecutor;
- mBackgroundThreadExecutor = backgroundThreadExecutor;
- mBoundaryCallback = boundaryCallback;
- mConfig = config;
- }
-
- /**
- * Create a PagedList which loads data from the provided data source on a background thread,
- * posting updates to the main thread.
- *
- *
- * @param dataSource DataSource providing data to the PagedList
- * @param mainThreadExecutor Thread that will use and consume data from the PagedList.
- * Generally, this is the UI/main thread.
- * @param backgroundThreadExecutor Data loading will be done via this executor - should be a
- * background thread.
- * @param boundaryCallback Optional boundary callback to attach to the list.
- * @param config PagedList Config, which defines how the PagedList will load data.
- * @param <K> Key type that indicates to the DataSource what data to load.
- * @param <T> Type of items to be held and loaded by the PagedList.
- *
- * @return Newly created PagedList, which will page in data from the DataSource as needed.
- */
- @NonNull
- private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
- @NonNull Executor mainThreadExecutor,
- @NonNull Executor backgroundThreadExecutor,
- @Nullable BoundaryCallback<T> boundaryCallback,
- @NonNull Config config,
- @Nullable K key) {
- if (dataSource.isContiguous() || !config.enablePlaceholders) {
- int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
- if (!dataSource.isContiguous()) {
- //noinspection unchecked
- dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
- .wrapAsContiguousWithoutPlaceholders();
- if (key != null) {
- lastLoad = (int) key;
- }
- }
- ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
- return new ContiguousPagedList<>(contigDataSource,
- mainThreadExecutor,
- backgroundThreadExecutor,
- boundaryCallback,
- config,
- key,
- lastLoad);
- } else {
- return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
- mainThreadExecutor,
- backgroundThreadExecutor,
- boundaryCallback,
- config,
- (key != null) ? (Integer) key : 0);
- }
- }
-
- /**
- * Builder class for PagedList.
- * <p>
- * DataSource, Config, main thread and background executor must all be provided.
- * <p>
- * A PagedList queries initial data from its DataSource during construction, to avoid empty
- * PagedLists being presented to the UI when possible. It's preferred 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>
- * {@link LivePagedListBuilder} 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.
- */
- @SuppressWarnings("WeakerAccess")
- public static final class Builder<Key, Value> {
- private final DataSource<Key, Value> mDataSource;
- private final Config mConfig;
- private Executor mMainThreadExecutor;
- private Executor mBackgroundThreadExecutor;
- private BoundaryCallback mBoundaryCallback;
- private Key mInitialKey;
-
- /**
- * Create a PagedList.Builder with the provided {@link DataSource} and {@link Config}.
- *
- * @param dataSource DataSource the PagedList will load from.
- * @param config Config that defines how the PagedList loads data from its DataSource.
- */
- public Builder(@NonNull DataSource<Key, Value> dataSource, @NonNull Config config) {
- //noinspection ConstantConditions
- if (dataSource == null) {
- throw new IllegalArgumentException("DataSource may not be null");
- }
- //noinspection ConstantConditions
- if (config == null) {
- throw new IllegalArgumentException("Config may not be null");
- }
- mDataSource = dataSource;
- mConfig = config;
- }
-
- /**
- * Create a PagedList.Builder with the provided {@link DataSource} and page size.
- * <p>
- * This method is a convenience for:
- * <pre>
- * PagedList.Builder(dataSource,
- * new PagedList.Config.Builder().setPageSize(pageSize).build());
- * </pre>
- *
- * @param dataSource DataSource the PagedList will load from.
- * @param pageSize Config that defines how the PagedList loads data from its DataSource.
- */
- public Builder(@NonNull DataSource<Key, Value> dataSource, int pageSize) {
- this(dataSource, new PagedList.Config.Builder().setPageSize(pageSize).build());
- }
- /**
- * The executor defining where main/UI thread for page loading updates.
- *
- * @param mainThreadExecutor Executor for main/UI thread to receive {@link Callback} calls.
- * @return this
- */
- @NonNull
- public Builder<Key, Value> setMainThreadExecutor(@NonNull Executor mainThreadExecutor) {
- mMainThreadExecutor = mainThreadExecutor;
- return this;
- }
-
- /**
- * The executor on which background loading will be run.
- * <p>
- * Does not affect initial load, which will be done on whichever thread the PagedList is
- * created on.
- *
- * @param backgroundThreadExecutor Executor for background DataSource loading.
- * @return this
- */
- @NonNull
- public Builder<Key, Value> setBackgroundThreadExecutor(
- @NonNull Executor backgroundThreadExecutor) {
- mBackgroundThreadExecutor = backgroundThreadExecutor;
- return this;
- }
-
- /**
- * The BoundaryCallback for out of data events.
- * <p>
- * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load.
- *
- * @param boundaryCallback BoundaryCallback for listening to out-of-data events.
- * @return this
- */
- @SuppressWarnings("unused")
- @NonNull
- public Builder<Key, Value> setBoundaryCallback(
- @Nullable BoundaryCallback boundaryCallback) {
- mBoundaryCallback = boundaryCallback;
- return this;
- }
-
- /**
- * Sets the initial key the DataSource should load around as part of initialization.
- *
- * @param initialKey Key the DataSource should load around as part of initialization.
- * @return this
- */
- @NonNull
- public Builder<Key, Value> setInitialKey(@Nullable Key initialKey) {
- mInitialKey = initialKey;
- return this;
- }
-
- /**
- * Creates a {@link PagedList} with the given parameters.
- * <p>
- * This call will dispatch the {@link DataSource}'s loadInitial method immediately. If a
- * DataSource posts all of its work (e.g. to a network thread), the PagedList will
- * be immediately created as empty, and grow to its initial size when the initial load
- * completes.
- * <p>
- * If the DataSource implements its load synchronously, doing the load work immediately in
- * the loadInitial method, the PagedList will block on that load before completing
- * construction. In this case, use a background thread to create a PagedList.
- * <p>
- * It's fine to create a PagedList with an async DataSource on the main thread, such as in
- * the constructor of a ViewModel. An async network load won't block the initialLoad
- * function. For a synchronous DataSource such as one created from a Room database, a
- * {@code LiveData<PagedList>} can be safely constructed with {@link LivePagedListBuilder}
- * on the main thread, since actual construction work is deferred, and done on a background
- * 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
- */
- @WorkerThread
- @NonNull
- public PagedList<Value> build() {
- // TODO: define defaults, once they can be used in module without android dependency
- if (mMainThreadExecutor == null) {
- throw new IllegalArgumentException("MainThreadExecutor required");
- }
- if (mBackgroundThreadExecutor == null) {
- throw new IllegalArgumentException("BackgroundThreadExecutor required");
- }
-
- //noinspection unchecked
- return PagedList.create(
- mDataSource,
- mMainThreadExecutor,
- mBackgroundThreadExecutor,
- mBoundaryCallback,
- mConfig,
- mInitialKey);
- }
- }
-
- /**
- * Get the item in the list of loaded items at the provided index.
- *
- * @param index Index in the loaded item list. Must be >= 0, and < {@link #size()}
- * @return The item at the passed index, or null if a null placeholder is at the specified
- * position.
- *
- * @see #size()
- */
- @Override
- @Nullable
- public T get(int index) {
- T item = mStorage.get(index);
- if (item != null) {
- mLastItem = item;
- }
- return item;
- }
-
- /**
- * Load adjacent items to passed index.
- *
- * @param index Index at which to load.
- */
- public void loadAround(int index) {
- mLastLoad = index + getPositionOffset();
- loadAroundInternal(index);
-
- mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
- mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
-
- /*
- * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
- * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
- * and accesses happen near the boundaries.
- *
- * Note: we post here, since RecyclerView may want to add items in response, and this
- * call occurs in PagedListAdapter bind.
- */
- tryDispatchBoundaryCallbacks(true);
- }
-
- // Creation thread for initial synchronous load, otherwise main thread
- // Safe to access main thread only state - no other thread has reference during construction
- @AnyThread
- void deferBoundaryCallbacks(final boolean deferEmpty,
- final boolean deferBegin, final boolean deferEnd) {
- if (mBoundaryCallback == null) {
- throw new IllegalStateException("Computing boundary");
- }
-
- /*
- * If lowest/highest haven't been initialized, set them to storage size,
- * since placeholders must already be computed by this point.
- *
- * This is just a minor optimization so that BoundaryCallback callbacks are sent immediately
- * if the initial load size is smaller than the prefetch window (see
- * TiledPagedListTest#boundaryCallback_immediate())
- */
- if (mLowestIndexAccessed == Integer.MAX_VALUE) {
- mLowestIndexAccessed = mStorage.size();
- }
- if (mHighestIndexAccessed == Integer.MIN_VALUE) {
- mHighestIndexAccessed = 0;
- }
-
- if (deferEmpty || deferBegin || deferEnd) {
- // Post to the main thread, since we may be on creation thread currently
- mMainThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- // on is dispatched immediately, since items won't be accessed
- //noinspection ConstantConditions
- if (deferEmpty) {
- mBoundaryCallback.onZeroItemsLoaded();
- }
-
- // for other callbacks, mark deferred, and only dispatch if loadAround
- // has been called near to the position
- if (deferBegin) {
- mBoundaryCallbackBeginDeferred = true;
- }
- if (deferEnd) {
- mBoundaryCallbackEndDeferred = true;
- }
- tryDispatchBoundaryCallbacks(false);
- }
- });
- }
- }
-
- /**
- * Call this when mLowest/HighestIndexAccessed are changed, or
- * mBoundaryCallbackBegin/EndDeferred is set.
- */
- private void tryDispatchBoundaryCallbacks(boolean post) {
- final boolean dispatchBegin = mBoundaryCallbackBeginDeferred
- && mLowestIndexAccessed <= mConfig.prefetchDistance;
- final boolean dispatchEnd = mBoundaryCallbackEndDeferred
- && mHighestIndexAccessed >= size() - mConfig.prefetchDistance;
-
- if (!dispatchBegin && !dispatchEnd) {
- return;
- }
-
- if (dispatchBegin) {
- mBoundaryCallbackBeginDeferred = false;
- }
- if (dispatchEnd) {
- mBoundaryCallbackEndDeferred = false;
- }
- if (post) {
- mMainThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
- }
- });
- } else {
- dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
- }
- }
-
- private void dispatchBoundaryCallbacks(boolean begin, boolean end) {
- // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present
- if (begin) {
- //noinspection ConstantConditions
- mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem());
- }
- if (end) {
- //noinspection ConstantConditions
- mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem());
- }
- }
-
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- void offsetBoundaryAccessIndices(int offset) {
- mLowestIndexAccessed += offset;
- mHighestIndexAccessed += offset;
- }
-
- /**
- * Returns size of the list, including any not-yet-loaded null padding.
- *
- * @return Current total size of the list.
- */
- @Override
- public int size() {
- return mStorage.size();
- }
-
- /**
- * Returns whether the list is immutable.
- *
- * Immutable lists may not become mutable again, and may safely be accessed from any thread.
- * <p>
- * In the future, this method may return true when a PagedList has completed loading from its
- * DataSource. Currently, it is equivalent to {@link #isDetached()}.
- *
- * @return True if the PagedList is immutable.
- */
- @SuppressWarnings("WeakerAccess")
- public boolean isImmutable() {
- return isDetached();
- }
-
- /**
- * Returns an immutable snapshot of the PagedList in its current state.
- *
- * If this PagedList {@link #isImmutable() is immutable} due to its DataSource being invalid, it
- * will be returned.
- *
- * @return Immutable snapshot of PagedList data.
- */
- @SuppressWarnings("WeakerAccess")
- @NonNull
- public List<T> snapshot() {
- if (isImmutable()) {
- return this;
- }
- return new SnapshotPagedList<>(this);
- }
-
- abstract boolean isContiguous();
-
- /**
- * Return the Config used to construct this PagedList.
- *
- * @return the Config of this PagedList
- */
- @NonNull
- public Config getConfig() {
- return mConfig;
- }
-
- /**
- * Return the DataSource that provides data to this PagedList.
- *
- * @return the DataSource of this PagedList.
- */
- @NonNull
- public abstract DataSource<?, T> getDataSource();
-
- /**
- * Return the key for the position passed most recently to {@link #loadAround(int)}.
- * <p>
- * When a PagedList is invalidated, you can pass the key returned by this function to initialize
- * the next PagedList. This ensures (depending on load times) that the next PagedList that
- * arrives will have data that overlaps. If you use {@link LivePagedListBuilder}, it will do
- * this for you.
- *
- * @return Key of position most recently passed to {@link #loadAround(int)}.
- */
- @Nullable
- public abstract Object getLastKey();
-
- /**
- * True if the PagedList has detached the DataSource it was loading from, and will no longer
- * load new data.
- * <p>
- * A detached list is {@link #isImmutable() immutable}.
- *
- * @return True if the data source is detached.
- */
- @SuppressWarnings("WeakerAccess")
- public boolean isDetached() {
- return mDetached.get();
- }
-
- /**
- * Detach the PagedList from its DataSource, and attempt to load no more data.
- * <p>
- * This is called automatically when a DataSource load returns <code>null</code>, which is a
- * signal to stop loading. The PagedList will continue to present existing data, but will not
- * initiate new loads.
- */
- @SuppressWarnings("WeakerAccess")
- public void detach() {
- mDetached.set(true);
- }
-
- /**
- * Position offset of the data in the list.
- * <p>
- * If data is supplied by a {@link PositionalDataSource}, the item returned from
- * <code>get(i)</code> has a position of <code>i + getPositionOffset()</code>.
- * <p>
- * If the DataSource is a {@link ItemKeyedDataSource} or {@link PageKeyedDataSource}, it
- * doesn't use positions, returns 0.
- */
- public int getPositionOffset() {
- return mStorage.getPositionOffset();
- }
-
- /**
- * Adds a callback, and issues updates since the previousSnapshot was created.
- * <p>
- * If previousSnapshot is passed, the callback will also immediately be dispatched any
- * differences between the previous snapshot, and the current state. For example, if the
- * previousSnapshot was of 5 nulls, 10 items, 5 nulls, and the current state was 5 nulls,
- * 12 items, 3 nulls, the callback would immediately receive a call of
- * <code>onChanged(14, 2)</code>.
- * <p>
- * This allows an observer that's currently presenting a snapshot to catch up to the most recent
- * version, including any changes that may have been made.
- * <p>
- * The callback is internally held as weak reference, so PagedList doesn't hold a strong
- * reference to its observer, such as a {@link PagedListAdapter}. If an adapter were held with a
- * strong reference, it would be necessary to clear its PagedList observer before it could be
- * GC'd.
- *
- * @param previousSnapshot Snapshot previously captured from this List, or null.
- * @param callback Callback to dispatch to.
- *
- * @see #removeWeakCallback(Callback)
- */
- @SuppressWarnings("WeakerAccess")
- public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
- if (previousSnapshot != null && previousSnapshot != this) {
-
- if (previousSnapshot.isEmpty()) {
- if (!mStorage.isEmpty()) {
- // If snapshot is empty, diff is trivial - just notify number new items.
- // Note: occurs in async init, when snapshot taken before init page arrives
- callback.onInserted(0, mStorage.size());
- }
- } else {
- PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot;
-
- //noinspection unchecked
- dispatchUpdatesSinceSnapshot(storageSnapshot, callback);
- }
- }
-
- // first, clean up any empty weak refs
- for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- Callback currentCallback = mCallbacks.get(i).get();
- if (currentCallback == null) {
- mCallbacks.remove(i);
- }
- }
-
- // then add the new one
- mCallbacks.add(new WeakReference<>(callback));
- }
- /**
- * Removes a previously added callback.
- *
- * @param callback Callback, previously added.
- * @see #addWeakCallback(List, Callback)
- */
- @SuppressWarnings("WeakerAccess")
- public void removeWeakCallback(@NonNull Callback callback) {
- for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- Callback currentCallback = mCallbacks.get(i).get();
- if (currentCallback == null || currentCallback == callback) {
- // found callback, or empty weak ref
- mCallbacks.remove(i);
- }
- }
- }
-
- void notifyInserted(int position, int count) {
- if (count != 0) {
- for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- Callback callback = mCallbacks.get(i).get();
- if (callback != null) {
- callback.onInserted(position, count);
- }
- }
- }
- }
-
- void notifyChanged(int position, int count) {
- if (count != 0) {
- for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- Callback callback = mCallbacks.get(i).get();
-
- if (callback != null) {
- callback.onChanged(position, count);
- }
- }
- }
- }
-
-
-
- /**
- * Dispatch updates since the non-empty snapshot was taken.
- *
- * @param snapshot Non-empty snapshot.
- * @param callback Callback for updates that have occurred since snapshot.
- */
- abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
- @NonNull Callback callback);
-
- abstract void loadAroundInternal(int index);
-
- /**
- * Callback signaling when content is loaded into the list.
- * <p>
- * Can be used to listen to items being paged in and out. These calls will be dispatched on
- * the executor defined by {@link Builder#setMainThreadExecutor(Executor)}, which defaults to
- * the main/UI thread.
- */
- public abstract static class Callback {
- /**
- * Called when null padding items have been loaded to signal newly available data, or when
- * data that hasn't been used in a while has been dropped, and swapped back to null.
- *
- * @param position Position of first newly loaded items, out of total number of items
- * (including padded nulls).
- * @param count Number of items loaded.
- */
- public abstract void onChanged(int position, int count);
-
- /**
- * Called when new items have been loaded at the end or beginning of the list.
- *
- * @param position Position of the first newly loaded item (in practice, either
- * <code>0</code> or <code>size - 1</code>.
- * @param count Number of items loaded.
- */
- public abstract void onInserted(int position, int count);
-
- /**
- * Called when items have been removed at the end or beginning of the list, and have not
- * been replaced by padded nulls.
- *
- * @param position Position of the first newly loaded item (in practice, either
- * <code>0</code> or <code>size - 1</code>.
- * @param count Number of items loaded.
- */
- @SuppressWarnings("unused")
- public abstract void onRemoved(int position, int count);
- }
-
- /**
- * Configures how a PagedList loads content from its DataSource.
- * <p>
- * Use a Config {@link Builder} to construct and define custom loading behavior, such as
- * {@link Builder#setPageSize(int)}, which defines number of items loaded at a time}.
- */
- public static class Config {
- /**
- * Size of each page loaded by the PagedList.
- */
- public final int pageSize;
-
- /**
- * Prefetch distance which defines how far ahead to load.
- * <p>
- * If this value is set to 50, the paged list will attempt to load 50 items in advance of
- * data that's already been accessed.
- *
- * @see PagedList#loadAround(int)
- */
- @SuppressWarnings("WeakerAccess")
- public final int prefetchDistance;
-
- /**
- * Defines whether the PagedList may display null placeholders, if the DataSource provides
- * them.
- */
- @SuppressWarnings("WeakerAccess")
- public final boolean enablePlaceholders;
-
- /**
- * Size hint for initial load of PagedList, often larger than a regular page.
- */
- @SuppressWarnings("WeakerAccess")
- public final int initialLoadSizeHint;
-
- private Config(int pageSize, int prefetchDistance,
- boolean enablePlaceholders, int initialLoadSizeHint) {
- this.pageSize = pageSize;
- this.prefetchDistance = prefetchDistance;
- this.enablePlaceholders = enablePlaceholders;
- this.initialLoadSizeHint = initialLoadSizeHint;
- }
-
- /**
- * Builder class for {@link Config}.
- * <p>
- * You must at minimum specify page size with {@link #setPageSize(int)}.
- */
- public static final class Builder {
- private int mPageSize = -1;
- private int mPrefetchDistance = -1;
- private int mInitialLoadSizeHint = -1;
- private boolean mEnablePlaceholders = true;
-
- /**
- * 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
- */
- public Builder setPageSize(int pageSize) {
- this.mPageSize = pageSize;
- return this;
- }
-
- /**
- * Defines how far from the edge of loaded content an access must be to trigger further
- * 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
- */
- public Builder setPrefetchDistance(int prefetchDistance) {
- this.mPrefetchDistance = prefetchDistance;
- return this;
- }
-
- /**
- * Pass false to disable null placeholders in PagedLists using this Config.
- * <p>
- * 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).
- * <p>
- * 2) placeholders are not disabled on the Config.
- * <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
- */
- @SuppressWarnings("SameParameterValue")
- public Builder setEnablePlaceholders(boolean enablePlaceholders) {
- this.mEnablePlaceholders = enablePlaceholders;
- return this;
- }
-
- /**
- * Defines how many items to load when first load occurs.
- * <p>
- * This value is typically larger than page size, so on first load data there's a large
- * enough range of content loaded to cover small scrolls.
- * <p>
- * When using a {@link PositionalDataSource}, the initial load size will be coerced to
- * an integer multiple of pageSize, to enable efficient tiling.
- * <p>
- * If not set, defaults to three times page size.
- *
- * @param initialLoadSizeHint Number of items to load while initializing the PagedList.
- * @return this
- */
- @SuppressWarnings("WeakerAccess")
- public Builder setInitialLoadSizeHint(int initialLoadSizeHint) {
- this.mInitialLoadSizeHint = initialLoadSizeHint;
- return this;
- }
-
- /**
- * Creates a {@link Config} with the given parameters.
- *
- * @return A new Config.
- */
- public Config build() {
- if (mPageSize < 1) {
- throw new IllegalArgumentException("Page size must be a positive number");
- }
- if (mPrefetchDistance < 0) {
- mPrefetchDistance = mPageSize;
- }
- if (mInitialLoadSizeHint < 0) {
- mInitialLoadSizeHint = mPageSize * 3;
- }
- if (!mEnablePlaceholders && mPrefetchDistance == 0) {
- throw new IllegalArgumentException("Placeholders and prefetch are the only ways"
- + " to trigger loading of more data in the PagedList, so either"
- + " placeholders must be enabled, or prefetch distance must be > 0.");
- }
-
- return new Config(mPageSize, mPrefetchDistance,
- mEnablePlaceholders, mInitialLoadSizeHint);
- }
- }
- }
-
- /**
- * Signals when a PagedList has reached the end of available data.
- * <p>
- * When local storage is a cache of network data, it's common to set up a streaming pipeline:
- * Network data is paged into the database, database is paged into UI. Paging from the database
- * to UI can be done with a {@code LiveData<PagedList>}, but it's still necessary to know when
- * to trigger network loads.
- * <p>
- * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of
- * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network
- * load that will write the result directly to the database. Because the database is being
- * observed, the UI bound to the {@code LiveData<PagedList>} will update automatically to
- * account for the new items.
- * <p>
- * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to
- * {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple
- * times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should
- * avoid triggering it again while the load is ongoing.
- * <p>
- * BoundaryCallback only passes the item at front or end of the list. Number of items is not
- * passed, since it may not be fully computed by the DataSource if placeholders are not
- * supplied. Keys are not known because the BoundaryCallback is independent of the
- * DataSource-specific keys, which may be different for local vs remote storage.
- * <p>
- * The database + network Repository in the
- * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
- * shows how to implement a network BoundaryCallback using
- * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
- * handling swipe-to-refresh, network errors, and retry.
- *
- * @param <T> Type loaded by the PagedList.
- */
- @MainThread
- public abstract static class BoundaryCallback<T> {
- /**
- * Called when zero items are returned from an initial load of the PagedList's data source.
- */
- public void onZeroItemsLoaded() {}
-
- /**
- * Called when the item at the front of the PagedList has been loaded, and access has
- * occurred within {@link Config#prefetchDistance} of it.
- * <p>
- * No more data will be prepended to the PagedList before this item.
- *
- * @param itemAtFront The first item of PagedList
- */
- public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}
-
- /**
- * Called when the item at the end of the PagedList has been loaded, and access has
- * occurred within {@link Config#prefetchDistance} of it.
- * <p>
- * No more data will be appended to the PagedList after this item.
- *
- * @param itemAtEnd The first item of PagedList
- */
- public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PagedStorage.java b/paging/common/src/main/java/android/arch/paging/PagedStorage.java
deleted file mode 100644
index d4531d3..0000000
--- a/paging/common/src/main/java/android/arch/paging/PagedStorage.java
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.AbstractList;
-import java.util.ArrayList;
-import java.util.List;
-
-final class PagedStorage<T> extends AbstractList<T> {
- /**
- * Lists instances are compared (with instance equality) to PLACEHOLDER_LIST to check if an item
- * in that position is already loading. We use a singleton placeholder list that is distinct
- * from Collections.EMPTY_LIST for safety.
- */
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- private static final List PLACEHOLDER_LIST = new ArrayList();
-
- // Always set
- private int mLeadingNullCount;
- /**
- * List of pages in storage.
- *
- * Two storage modes:
- *
- * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled().
- * Safe to access any item in any page.
- *
- * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true.
- * mPages may have nulls, or placeholder (empty) pages while content is loading.
- */
- private final ArrayList<List<T>> mPages;
- private int mTrailingNullCount;
-
- private int mPositionOffset;
- /**
- * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in
- * {@link #mPages} may be null, but this value still counts them.
- */
- private int mStorageCount;
-
- // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set
- private int mPageSize;
-
- private int mNumberPrepended;
- private int mNumberAppended;
-
- PagedStorage() {
- mLeadingNullCount = 0;
- mPages = new ArrayList<>();
- mTrailingNullCount = 0;
- mPositionOffset = 0;
- mStorageCount = 0;
- mPageSize = 1;
- mNumberPrepended = 0;
- mNumberAppended = 0;
- }
-
- PagedStorage(int leadingNulls, List<T> page, int trailingNulls) {
- this();
- init(leadingNulls, page, trailingNulls, 0);
- }
-
- private PagedStorage(PagedStorage<T> other) {
- mLeadingNullCount = other.mLeadingNullCount;
- mPages = new ArrayList<>(other.mPages);
- mTrailingNullCount = other.mTrailingNullCount;
- mPositionOffset = other.mPositionOffset;
- mStorageCount = other.mStorageCount;
- mPageSize = other.mPageSize;
- mNumberPrepended = other.mNumberPrepended;
- mNumberAppended = other.mNumberAppended;
- }
-
- PagedStorage<T> snapshot() {
- return new PagedStorage<>(this);
- }
-
- private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) {
- mLeadingNullCount = leadingNulls;
- mPages.clear();
- mPages.add(page);
- mTrailingNullCount = trailingNulls;
-
- mPositionOffset = positionOffset;
- mStorageCount = page.size();
-
- // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled
- // even if it will break if nulls convert.
- mPageSize = page.size();
-
- mNumberPrepended = 0;
- mNumberAppended = 0;
- }
-
- void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
- @NonNull Callback callback) {
- init(leadingNulls, page, trailingNulls, positionOffset);
- callback.onInitialized(size());
- }
-
- @Override
- public T get(int i) {
- if (i < 0 || i >= size()) {
- throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size());
- }
-
- // is it definitely outside 'mPages'?
- int localIndex = i - mLeadingNullCount;
- if (localIndex < 0 || localIndex >= mStorageCount) {
- return null;
- }
-
- int localPageIndex;
- int pageInternalIndex;
-
- if (isTiled()) {
- // it's inside mPages, and we're tiled. Jump to correct tile.
- localPageIndex = localIndex / mPageSize;
- pageInternalIndex = localIndex % mPageSize;
- } else {
- // it's inside mPages, but page sizes aren't regular. Walk to correct tile.
- // Pages can only be null while tiled, so accessing page count is safe.
- pageInternalIndex = localIndex;
- final int localPageCount = mPages.size();
- for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) {
- int pageSize = mPages.get(localPageIndex).size();
- if (pageSize > pageInternalIndex) {
- // stop, found the page
- break;
- }
- pageInternalIndex -= pageSize;
- }
- }
-
- List<T> page = mPages.get(localPageIndex);
- if (page == null || page.size() == 0) {
- // can only occur in tiled case, with untouched inner/placeholder pages
- return null;
- }
- return page.get(pageInternalIndex);
- }
-
- /**
- * Returns true if all pages are the same size, except for the last, which may be smaller
- */
- boolean isTiled() {
- return mPageSize > 0;
- }
-
- int getLeadingNullCount() {
- return mLeadingNullCount;
- }
-
- int getTrailingNullCount() {
- return mTrailingNullCount;
- }
-
- int getStorageCount() {
- return mStorageCount;
- }
-
- int getNumberAppended() {
- return mNumberAppended;
- }
-
- int getNumberPrepended() {
- return mNumberPrepended;
- }
-
- int getPageCount() {
- return mPages.size();
- }
-
- interface Callback {
- void onInitialized(int count);
- void onPagePrepended(int leadingNulls, int changed, int added);
- void onPageAppended(int endPosition, int changed, int added);
- void onPagePlaceholderInserted(int pageIndex);
- void onPageInserted(int start, int count);
- }
-
- int getPositionOffset() {
- return mPositionOffset;
- }
-
- @Override
- public int size() {
- return mLeadingNullCount + mStorageCount + mTrailingNullCount;
- }
-
- int computeLeadingNulls() {
- int total = mLeadingNullCount;
- final int pageCount = mPages.size();
- for (int i = 0; i < pageCount; i++) {
- List page = mPages.get(i);
- if (page != null && page != PLACEHOLDER_LIST) {
- break;
- }
- total += mPageSize;
- }
- return total;
- }
-
- int computeTrailingNulls() {
- int total = mTrailingNullCount;
- for (int i = mPages.size() - 1; i >= 0; i--) {
- List page = mPages.get(i);
- if (page != null && page != PLACEHOLDER_LIST) {
- break;
- }
- total += mPageSize;
- }
- return total;
- }
-
- // ---------------- Contiguous API -------------------
-
- T getFirstLoadedItem() {
- // safe to access first page's first item here:
- // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
- return mPages.get(0).get(0);
- }
-
- T getLastLoadedItem() {
- // safe to access last page's last item here:
- // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
- List<T> page = mPages.get(mPages.size() - 1);
- return page.get(page.size() - 1);
- }
-
- void prependPage(@NonNull List<T> page, @NonNull Callback callback) {
- final int count = page.size();
- if (count == 0) {
- // Nothing returned from source, stop loading in this direction
- return;
- }
- if (mPageSize > 0 && count != mPageSize) {
- if (mPages.size() == 1 && count > mPageSize) {
- // prepending to a single item - update current page size to that of 'inner' page
- mPageSize = count;
- } else {
- // no longer tiled
- mPageSize = -1;
- }
- }
-
- mPages.add(0, page);
- mStorageCount += count;
-
- final int changedCount = Math.min(mLeadingNullCount, count);
- final int addedCount = count - changedCount;
-
- if (changedCount != 0) {
- mLeadingNullCount -= changedCount;
- }
- mPositionOffset -= addedCount;
- mNumberPrepended += count;
-
- callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount);
- }
-
- void appendPage(@NonNull List<T> page, @NonNull Callback callback) {
- final int count = page.size();
- if (count == 0) {
- // Nothing returned from source, stop loading in this direction
- return;
- }
-
- if (mPageSize > 0) {
- // if the previous page was smaller than mPageSize,
- // or if this page is larger than the previous, disable tiling
- if (mPages.get(mPages.size() - 1).size() != mPageSize
- || count > mPageSize) {
- mPageSize = -1;
- }
- }
-
- mPages.add(page);
- mStorageCount += count;
-
- final int changedCount = Math.min(mTrailingNullCount, count);
- final int addedCount = count - changedCount;
-
- if (changedCount != 0) {
- mTrailingNullCount -= changedCount;
- }
- mNumberAppended += count;
- callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
- changedCount, addedCount);
- }
-
- // ------------------ Non-Contiguous API (tiling required) ----------------------
-
- void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList,
- int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) {
-
- int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize;
- for (int i = 0; i < pageCount; i++) {
- int beginInclusive = i * pageSize;
- int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize);
-
- List<T> sublist = multiPageList.subList(beginInclusive, endExclusive);
-
- if (i == 0) {
- // Trailing nulls for first page includes other pages in multiPageList
- int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size();
- init(leadingNulls, sublist, initialTrailingNulls, positionOffset);
- } else {
- int insertPosition = leadingNulls + beginInclusive;
- insertPage(insertPosition, sublist, null);
- }
- }
- callback.onInitialized(size());
- }
-
- public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) {
- final int newPageSize = page.size();
- if (newPageSize != mPageSize) {
- // differing page size is OK in 2 cases, when the page is being added:
- // 1) to the end (in which case, ignore new smaller size)
- // 2) only the last page has been added so far (in which case, adopt new bigger size)
-
- int size = size();
- boolean addingLastPage = position == (size - size % mPageSize)
- && newPageSize < mPageSize;
- boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
- && newPageSize > mPageSize;
-
- // OK only if existing single page, and it's the last one
- if (!onlyEndPagePresent && !addingLastPage) {
- throw new IllegalArgumentException("page introduces incorrect tiling");
- }
- if (onlyEndPagePresent) {
- mPageSize = newPageSize;
- }
- }
-
- int pageIndex = position / mPageSize;
-
- allocatePageRange(pageIndex, pageIndex);
-
- int localPageIndex = pageIndex - mLeadingNullCount / mPageSize;
-
- List<T> oldPage = mPages.get(localPageIndex);
- if (oldPage != null && oldPage != PLACEHOLDER_LIST) {
- throw new IllegalArgumentException(
- "Invalid position " + position + ": data already loaded");
- }
- mPages.set(localPageIndex, page);
- if (callback != null) {
- callback.onPageInserted(position, page.size());
- }
- }
-
- private void allocatePageRange(final int minimumPage, final int maximumPage) {
- int leadingNullPages = mLeadingNullCount / mPageSize;
-
- if (minimumPage < leadingNullPages) {
- for (int i = 0; i < leadingNullPages - minimumPage; i++) {
- mPages.add(0, null);
- }
- int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize;
- mStorageCount += newStorageAllocated;
- mLeadingNullCount -= newStorageAllocated;
-
- leadingNullPages = minimumPage;
- }
- if (maximumPage >= leadingNullPages + mPages.size()) {
- int newStorageAllocated = Math.min(mTrailingNullCount,
- (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize);
- for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) {
- mPages.add(mPages.size(), null);
- }
- mStorageCount += newStorageAllocated;
- mTrailingNullCount -= newStorageAllocated;
- }
- }
-
- public void allocatePlaceholders(int index, int prefetchDistance,
- int pageSize, Callback callback) {
- if (pageSize != mPageSize) {
- if (pageSize < mPageSize) {
- throw new IllegalArgumentException("Page size cannot be reduced");
- }
- if (mPages.size() != 1 || mTrailingNullCount != 0) {
- // not in single, last page allocated case - can't change page size
- throw new IllegalArgumentException(
- "Page size can change only if last page is only one present");
- }
- mPageSize = pageSize;
- }
-
- final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
- int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
- int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
-
- allocatePageRange(minimumPage, maximumPage);
- int leadingNullPages = mLeadingNullCount / mPageSize;
- for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
- int localPageIndex = pageIndex - leadingNullPages;
- if (mPages.get(localPageIndex) == null) {
- //noinspection unchecked
- mPages.set(localPageIndex, PLACEHOLDER_LIST);
- callback.onPagePlaceholderInserted(pageIndex);
- }
- }
- }
-
- public boolean hasPage(int pageSize, int index) {
- // NOTE: we pass pageSize here to avoid in case mPageSize
- // not fully initialized (when last page only one loaded)
- int leadingNullPages = mLeadingNullCount / pageSize;
-
- if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) {
- return false;
- }
-
- List<T> page = mPages.get(index - leadingNullPages);
-
- return page != null && page != PLACEHOLDER_LIST;
- }
-
- @Override
- public String toString() {
- StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount
- + ", storage " + mStorageCount
- + ", trailing " + getTrailingNullCount());
-
- for (int i = 0; i < mPages.size(); i++) {
- ret.append(" ").append(mPages.get(i));
- }
- return ret.toString();
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
deleted file mode 100644
index 1fe6725..0000000
--- a/paging/common/src/main/java/android/arch/paging/PositionalDataSource.java
+++ /dev/null
@@ -1,546 +0,0 @@
-/*
- * Copyright 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
- * arbitrary page positions.
- * <p>
- * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. If your data source can't support loading arbitrary
- * requested page sizes (e.g. when network page size constraints are only known at runtime), use
- * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
- * <p>
- * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
- * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
- * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
- * If placeholders are disabled, initialize with the two parameter
- * {@link LoadInitialCallback#onResult(List, int)}.
- * <p>
- * Room can generate a Factory of PositionalDataSources for you:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
- * public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
- * }</pre>
- *
- * @param <T> Type of items being loaded by the PositionalDataSource.
- */
-public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
-
- /**
- * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadInitialParams {
- /**
- * Initial load position requested.
- * <p>
- * Note that this may not be within the bounds of your data set, it may need to be adjusted
- * before you execute your load.
- */
- public final int requestedStartPosition;
-
- /**
- * Requested number of items to load.
- * <p>
- * Note that this may be larger than available data.
- */
- public final int requestedLoadSize;
-
- /**
- * Defines page size acceptable for return values.
- * <p>
- * List of items passed to the callback must be an integer multiple of page size.
- */
- public final int pageSize;
-
- /**
- * Defines whether placeholders are enabled, and whether the total count passed to
- * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
- */
- public final boolean placeholdersEnabled;
-
- public LoadInitialParams(
- int requestedStartPosition,
- int requestedLoadSize,
- int pageSize,
- boolean placeholdersEnabled) {
- this.requestedStartPosition = requestedStartPosition;
- this.requestedLoadSize = requestedLoadSize;
- this.pageSize = pageSize;
- this.placeholdersEnabled = placeholdersEnabled;
- }
- }
-
- /**
- * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadRangeParams {
- /**
- * Start position of data to load.
- * <p>
- * Returned data must start at this position.
- */
- public final int startPosition;
- /**
- * Number of items to load.
- * <p>
- * Returned data must be of this size, unless at end of the list.
- */
- public final int loadSize;
-
- public LoadRangeParams(int startPosition, int loadSize) {
- this.startPosition = startPosition;
- this.loadSize = loadSize;
- }
- }
-
- /**
- * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
- * to return data, position, and count.
- * <p>
- * A callback should be called only once, and may throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <T> Type of items being loaded.
- */
- public abstract static class LoadInitialCallback<T> {
- /**
- * Called to pass initial load state from a DataSource.
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * and inform how many placeholders should be shown before and after. If counting is cheap
- * to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass the total size to the totalCount parameter. If placeholders are not
- * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
- * call {@link #onResult(List, int)}.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be loaded from this DataSource,
- * pass {@code N}.
- * @param totalCount Total number of items that may be returned from this DataSource.
- * Includes the number in the initial {@code data} parameter
- * as well as any items that can be loaded in front or behind of
- * {@code data}.
- */
- public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
-
- /**
- * Called to pass initial load state from a DataSource without total count,
- * when placeholders aren't requested.
- * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
- * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * if position is known but total size is not. If placeholders are requested, call the three
- * parameter variant: {@link #onResult(List, int, int)}.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be provided by this DataSource,
- * pass {@code N}.
- */
- public abstract void onResult(@NonNull List<T> data, int position);
- }
-
- /**
- * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
- * to return data.
- * <p>
- * A callback should be called only once, and may throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <T> Type of items being loaded.
- */
- public abstract static class LoadRangeCallback<T> {
- /**
- * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
- *
- * @param data List of items loaded from the DataSource. Must be same size as requested,
- * unless at end of list.
- */
- public abstract void onResult(@NonNull List<T> data);
- }
-
- static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
- final LoadCallbackHelper<T> mCallbackHelper;
- private final boolean mCountingEnabled;
- private final int mPageSize;
-
- LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
- int pageSize, PageResult.Receiver<T> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
- mCountingEnabled = countingEnabled;
- mPageSize = pageSize;
- if (mPageSize < 1) {
- throw new IllegalArgumentException("Page size must be non-negative");
- }
- }
-
- @Override
- public void onResult(@NonNull List<T> data, int position, int totalCount) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
- if (position + data.size() != totalCount
- && data.size() % mPageSize != 0) {
- throw new IllegalArgumentException("PositionalDataSource requires initial load"
- + " size to be a multiple of page size to support internal tiling."
- + " loadSize " + data.size() + ", position " + position
- + ", totalCount " + totalCount + ", pageSize " + mPageSize);
- }
-
- if (mCountingEnabled) {
- int trailingUnloadedCount = totalCount - position - data.size();
- mCallbackHelper.dispatchResultToReceiver(
- new PageResult<>(data, position, trailingUnloadedCount, 0));
- } else {
- // Only occurs when wrapped as contiguous
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- @Override
- public void onResult(@NonNull List<T> data, int position) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative");
- }
- if (data.isEmpty() && position != 0) {
- throw new IllegalArgumentException(
- "Initial result cannot be empty if items are present in data set.");
- }
- if (mCountingEnabled) {
- throw new IllegalStateException("Placeholders requested, but totalCount not"
- + " provided. Please call the three-parameter onResult method, or"
- + " disable placeholders in the PagedList.Config");
- }
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- static class LoadRangeCallbackImpl<T> extends LoadRangeCallback<T> {
- private LoadCallbackHelper<T> mCallbackHelper;
- private final int mPositionOffset;
- LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
- @PageResult.ResultType int resultType, int positionOffset,
- Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(
- dataSource, resultType, mainThreadExecutor, receiver);
- mPositionOffset = positionOffset;
- }
-
- @Override
- public void onResult(@NonNull List<T> data) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
- data, 0, 0, mPositionOffset));
- }
- }
- }
-
- final void dispatchLoadInitial(boolean acceptCount,
- int requestedStartPosition, int requestedLoadSize, int pageSize,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- LoadInitialCallbackImpl<T> callback =
- new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
-
- LoadInitialParams params = new LoadInitialParams(
- requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
- loadInitial(params, callback);
-
- // If initialLoad's callback is not called within the body, we force any following calls
- // to post to the UI thread. This constructor may be run on a background thread, but
- // after constructor, mutation must happen on UI thread.
- callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
- }
-
- final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
- int count, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<T> receiver) {
- LoadRangeCallback<T> callback = new LoadRangeCallbackImpl<>(
- this, resultType, startPosition, mainThreadExecutor, receiver);
- if (count == 0) {
- callback.onResult(Collections.<T>emptyList());
- } else {
- loadRange(new LoadRangeParams(startPosition, count), callback);
- }
- }
-
- /**
- * Load initial list data.
- * <p>
- * This method is called to load the initial page(s) from the DataSource.
- * <p>
- * Result list must be a multiple of pageSize to enable efficient tiling.
- *
- * @param params Parameters for initial load, including requested start position, load size, and
- * page size.
- * @param callback Callback that receives initial load data, including
- * position and total data set size.
- */
- @WorkerThread
- public abstract void loadInitial(
- @NonNull LoadInitialParams params,
- @NonNull LoadInitialCallback<T> callback);
-
- /**
- * Called to load a range of data from the DataSource.
- * <p>
- * This method is called to load additional pages from the DataSource after the
- * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
- * <p>
- * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
- * the number of items requested, at the position requested.
- *
- * @param params Parameters for load, including start position and load size.
- * @param callback Callback that receives loaded data.
- */
- @WorkerThread
- public abstract void loadRange(@NonNull LoadRangeParams params,
- @NonNull LoadRangeCallback<T> callback);
-
- @Override
- boolean isContiguous() {
- return false;
- }
-
- @NonNull
- ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
- return new ContiguousWithoutPlaceholdersWrapper<>(this);
- }
-
- /**
- * Helper for computing an initial position in
- * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
- * computed ahead of loading.
- * <p>
- * The value computed by this function will do bounds checking, page alignment, and positioning
- * based on initial load size requested.
- * <p>
- * Example usage in a PositionalDataSource subclass:
- * <pre>
- * class ItemDataSource extends PositionalDataSource<Item> {
- * private int computeCount() {
- * // actual count code here
- * }
- *
- * private List<Item> loadRangeInternal(int startPosition, int loadCount) {
- * // actual load code here
- * }
- *
- * {@literal @}Override
- * public void loadInitial({@literal @}NonNull LoadInitialParams params,
- * {@literal @}NonNull LoadInitialCallback<Item> callback) {
- * int totalCount = computeCount();
- * int position = computeInitialLoadPosition(params, totalCount);
- * int loadSize = computeInitialLoadSize(params, position, totalCount);
- * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
- * }
- *
- * {@literal @}Override
- * public void loadRange({@literal @}NonNull LoadRangeParams params,
- * {@literal @}NonNull LoadRangeCallback<Item> callback) {
- * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
- * }
- * }</pre>
- *
- * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
- * including page size, and requested start/loadSize.
- * @param totalCount Total size of the data set.
- * @return Position to start loading at.
- *
- * @see #computeInitialLoadSize(LoadInitialParams, int, int)
- */
- public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
- int totalCount) {
- int position = params.requestedStartPosition;
- int initialLoadSize = params.requestedLoadSize;
- int pageSize = params.pageSize;
-
- int roundedPageStart = Math.round(position / pageSize) * pageSize;
-
- // maximum start pos is that which will encompass end of list
- int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
- roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
-
- // minimum start position is 0
- roundedPageStart = Math.max(0, roundedPageStart);
-
- return roundedPageStart;
- }
-
- /**
- * Helper for computing an initial load size in
- * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
- * computed ahead of loading.
- * <p>
- * This function takes the requested load size, and bounds checks it against the value returned
- * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
- * <p>
- * Example usage in a PositionalDataSource subclass:
- * <pre>
- * class ItemDataSource extends PositionalDataSource<Item> {
- * private int computeCount() {
- * // actual count code here
- * }
- *
- * private List<Item> loadRangeInternal(int startPosition, int loadCount) {
- * // actual load code here
- * }
- *
- * {@literal @}Override
- * public void loadInitial({@literal @}NonNull LoadInitialParams params,
- * {@literal @}NonNull LoadInitialCallback<Item> callback) {
- * int totalCount = computeCount();
- * int position = computeInitialLoadPosition(params, totalCount);
- * int loadSize = computeInitialLoadSize(params, position, totalCount);
- * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
- * }
- *
- * {@literal @}Override
- * public void loadRange({@literal @}NonNull LoadRangeParams params,
- * {@literal @}NonNull LoadRangeCallback<Item> callback) {
- * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
- * }
- * }</pre>
- *
- * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
- * including page size, and requested start/loadSize.
- * @param initialLoadPosition Value returned by
- * {@link #computeInitialLoadPosition(LoadInitialParams, int)}
- * @param totalCount Total size of the data set.
- * @return Number of items to load.
- *
- * @see #computeInitialLoadPosition(LoadInitialParams, int)
- */
- @SuppressWarnings("WeakerAccess")
- public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
- int initialLoadPosition, int totalCount) {
- return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
- }
-
- @SuppressWarnings("deprecation")
- static class ContiguousWithoutPlaceholdersWrapper<Value>
- extends ContiguousDataSource<Integer, Value> {
-
- @NonNull
- final PositionalDataSource<Value> mPositionalDataSource;
-
- ContiguousWithoutPlaceholdersWrapper(
- @NonNull PositionalDataSource<Value> positionalDataSource) {
- mPositionalDataSource = positionalDataSource;
- }
-
- @NonNull
- @Override
- public <ToValue> DataSource<Integer, ToValue> mapByPage(
- @NonNull Function<List<Value>, List<ToValue>> function) {
- throw new UnsupportedOperationException(
- "Inaccessible inner type doesn't support map op");
- }
-
- @NonNull
- @Override
- public <ToValue> DataSource<Integer, ToValue> map(
- @NonNull Function<Value, ToValue> function) {
- throw new UnsupportedOperationException(
- "Inaccessible inner type doesn't support map op");
- }
-
- @Override
- void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
- boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- final int convertPosition = position == null ? 0 : position;
-
- // Note enablePlaceholders will be false here, but we don't have a way to communicate
- // this to PositionalDataSource. This is fine, because only the list and its position
- // offset will be consumed by the LoadInitialCallback.
- mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
- pageSize, mainThreadExecutor, receiver);
- }
-
- @Override
- void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
- @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- int startIndex = currentEndIndex + 1;
- mPositionalDataSource.dispatchLoadRange(
- PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
- }
-
- @Override
- void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
- int pageSize, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
-
- int startIndex = currentBeginIndex - 1;
- if (startIndex < 0) {
- // trigger empty list load
- mPositionalDataSource.dispatchLoadRange(
- PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
- } else {
- int loadSize = Math.min(pageSize, startIndex + 1);
- startIndex = startIndex - loadSize + 1;
- mPositionalDataSource.dispatchLoadRange(
- PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
- }
- }
-
- @Override
- Integer getKey(int position, Value item) {
- return position;
- }
-
- }
-
- @NonNull
- @Override
- public final <V> PositionalDataSource<V> mapByPage(
- @NonNull Function<List<T>, List<V>> function) {
- return new WrapperPositionalDataSource<>(this, function);
- }
-
- @NonNull
- @Override
- public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
- return mapByPage(createListFunction(function));
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java b/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
deleted file mode 100644
index 4627d2a..0000000
--- a/paging/common/src/main/java/android/arch/paging/SnapshotPagedList.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-class SnapshotPagedList<T> extends PagedList<T> {
- private final boolean mContiguous;
- private final Object mLastKey;
- private final DataSource<?, T> mDataSource;
-
- SnapshotPagedList(@NonNull PagedList<T> pagedList) {
- super(pagedList.mStorage.snapshot(),
- pagedList.mMainThreadExecutor,
- pagedList.mBackgroundThreadExecutor,
- null,
- pagedList.mConfig);
- mDataSource = pagedList.getDataSource();
- mContiguous = pagedList.isContiguous();
- mLastKey = pagedList.getLastKey();
- }
-
- @Override
- public boolean isImmutable() {
- return true;
- }
-
- @Override
- public boolean isDetached() {
- return true;
- }
-
- @Override
- boolean isContiguous() {
- return mContiguous;
- }
-
- @Nullable
- @Override
- public Object getLastKey() {
- return mLastKey;
- }
-
- @NonNull
- @Override
- public DataSource<?, T> getDataSource() {
- return mDataSource;
- }
-
- @Override
- void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot,
- @NonNull Callback callback) {
- }
-
- @Override
- void loadAroundInternal(int index) {
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java b/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
deleted file mode 100644
index 7285aa4..0000000
--- a/paging/common/src/main/java/android/arch/paging/TiledDataSource.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.WorkerThread;
-
-import java.util.Collections;
-import java.util.List;
-
-// NOTE: Room 1.0 depends on this class, so it should not be removed until
-// we can require a version of Room that uses PositionalDataSource directly
-/**
- * @param <T> Type loaded by the TiledDataSource.
- *
- * @deprecated Use {@link PositionalDataSource}
- * @hide
- */
-@SuppressWarnings("DeprecatedIsStillUsed")
-@Deprecated
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
-
- @WorkerThread
- public abstract int countItems();
-
- @Override
- boolean isContiguous() {
- return false;
- }
-
- @WorkerThread
- public abstract List<T> loadRange(int startPosition, int count);
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams params,
- @NonNull LoadInitialCallback<T> callback) {
- int totalCount = countItems();
- if (totalCount == 0) {
- callback.onResult(Collections.<T>emptyList(), 0, 0);
- return;
- }
-
- // bound the size requested, based on known count
- final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
- final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
-
- // convert from legacy behavior
- List<T> list = loadRange(firstLoadPosition, firstLoadSize);
- if (list != null && list.size() == firstLoadSize) {
- callback.onResult(list, firstLoadPosition, totalCount);
- } else {
- // null list, or size doesn't match request
- // The size check is a WAR for Room 1.0, subsequent versions do the check in Room
- invalidate();
- }
- }
-
- @Override
- public void loadRange(@NonNull LoadRangeParams params,
- @NonNull LoadRangeCallback<T> callback) {
- List<T> list = loadRange(params.startPosition, params.loadSize);
- if (list != null) {
- callback.onResult(list);
- } else {
- invalidate();
- }
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java b/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
deleted file mode 100644
index 374fe2a..0000000
--- a/paging/common/src/main/java/android/arch/paging/TiledPagedList.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.AnyThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-
-import java.util.concurrent.Executor;
-
-class TiledPagedList<T> extends PagedList<T>
- implements PagedStorage.Callback {
- private final PositionalDataSource<T> mDataSource;
-
- private PageResult.Receiver<T> mReceiver = new PageResult.Receiver<T>() {
- // Creation thread for initial synchronous load, otherwise main thread
- // Safe to access main thread only state - no other thread has reference during construction
- @AnyThread
- @Override
- public void onPageResult(@PageResult.ResultType int type,
- @NonNull PageResult<T> pageResult) {
- if (pageResult.isInvalid()) {
- detach();
- return;
- }
-
- if (isDetached()) {
- // No op, have detached
- return;
- }
-
- if (type != PageResult.INIT && type != PageResult.TILE) {
- throw new IllegalArgumentException("unexpected resultType" + type);
- }
-
- if (mStorage.getPageCount() == 0) {
- mStorage.initAndSplit(
- pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
- pageResult.positionOffset, mConfig.pageSize, TiledPagedList.this);
- } else {
- mStorage.insertPage(pageResult.positionOffset, pageResult.page,
- TiledPagedList.this);
- }
-
- if (mBoundaryCallback != null) {
- boolean deferEmpty = mStorage.size() == 0;
- boolean deferBegin = !deferEmpty
- && pageResult.leadingNulls == 0
- && pageResult.positionOffset == 0;
- int size = size();
- boolean deferEnd = !deferEmpty
- && ((type == PageResult.INIT && pageResult.trailingNulls == 0)
- || (type == PageResult.TILE
- && pageResult.positionOffset
- == (size - size % mConfig.pageSize)));
- deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
- }
- }
- };
-
- @WorkerThread
- TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
- @NonNull Executor mainThreadExecutor,
- @NonNull Executor backgroundThreadExecutor,
- @Nullable BoundaryCallback<T> boundaryCallback,
- @NonNull Config config,
- int position) {
- super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
- boundaryCallback, config);
- mDataSource = dataSource;
-
- final int pageSize = mConfig.pageSize;
- mLastLoad = position;
-
- if (mDataSource.isInvalid()) {
- detach();
- } else {
- final int firstLoadSize =
- (Math.max(Math.round(mConfig.initialLoadSizeHint / pageSize), 2)) * pageSize;
-
- final int idealStart = position - firstLoadSize / 2;
- final int roundedPageStart = Math.max(0, Math.round(idealStart / pageSize) * pageSize);
-
- mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
- pageSize, mMainThreadExecutor, mReceiver);
- }
- }
-
- @Override
- boolean isContiguous() {
- return false;
- }
-
- @NonNull
- @Override
- public DataSource<?, T> getDataSource() {
- return mDataSource;
- }
-
- @Nullable
- @Override
- public Object getLastKey() {
- return mLastLoad;
- }
-
- @Override
- protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
- @NonNull Callback callback) {
- //noinspection UnnecessaryLocalVariable
- final PagedStorage<T> snapshot = pagedListSnapshot.mStorage;
-
- if (snapshot.isEmpty()
- || mStorage.size() != snapshot.size()) {
- throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
- + " to be a snapshot of this PagedList");
- }
-
- // loop through each page and signal the callback for any pages that are present now,
- // but not in the snapshot.
- final int pageSize = mConfig.pageSize;
- final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize;
- final int pageCount = mStorage.getPageCount();
- for (int i = 0; i < pageCount; i++) {
- int pageIndex = i + leadingNullPages;
- int updatedPages = 0;
- // count number of consecutive pages that were added since the snapshot...
- while (updatedPages < mStorage.getPageCount()
- && mStorage.hasPage(pageSize, pageIndex + updatedPages)
- && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) {
- updatedPages++;
- }
- // and signal them all at once to the callback
- if (updatedPages > 0) {
- callback.onChanged(pageIndex * pageSize, pageSize * updatedPages);
- i += updatedPages - 1;
- }
- }
- }
-
- @Override
- protected void loadAroundInternal(int index) {
- mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this);
- }
-
- @Override
- public void onInitialized(int count) {
- notifyInserted(0, count);
- }
-
- @Override
- public void onPagePrepended(int leadingNulls, int changed, int added) {
- throw new IllegalStateException("Contiguous callback on TiledPagedList");
- }
-
- @Override
- public void onPageAppended(int endPosition, int changed, int added) {
- throw new IllegalStateException("Contiguous callback on TiledPagedList");
- }
-
- @Override
- public void onPagePlaceholderInserted(final int pageIndex) {
- // placeholder means initialize a load
- mBackgroundThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (isDetached()) {
- return;
- }
- final int pageSize = mConfig.pageSize;
-
- if (mDataSource.isInvalid()) {
- detach();
- } else {
- int startPosition = pageIndex * pageSize;
- int count = Math.min(pageSize, mStorage.size() - startPosition);
- mDataSource.dispatchLoadRange(
- PageResult.TILE, startPosition, count, mMainThreadExecutor, mReceiver);
- }
- }
- });
- }
-
- @Override
- public void onPageInserted(int start, int count) {
- notifyChanged(start, count);
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java
deleted file mode 100644
index 93520ca..0000000
--- a/paging/common/src/main/java/android/arch/paging/WrapperItemKeyedDataSource.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright 2018 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.NonNull;
-
-import java.util.IdentityHashMap;
-import java.util.List;
-
-class WrapperItemKeyedDataSource<K, A, B> extends ItemKeyedDataSource<K, B> {
- private final ItemKeyedDataSource<K, A> mSource;
- private final Function<List<A>, List<B>> mListFunction;
- private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- removeCallback();
- }
- };
-
- private final IdentityHashMap<B, K> mKeyMap = new IdentityHashMap<>();
-
- WrapperItemKeyedDataSource(ItemKeyedDataSource<K, A> source,
- Function<List<A>, List<B>> listFunction) {
- mSource = source;
- mListFunction = listFunction;
- mSource.addInvalidatedCallback(mInvalidatedCallback);
- }
-
- private void removeCallback() {
- mSource.removeInvalidatedCallback(mInvalidatedCallback);
- }
-
- private List<B> convertWithStashedKeys(List<A> source) {
- List<B> dest = convert(mListFunction, source);
- synchronized (mKeyMap) {
- // synchronize on mKeyMap, since multiple loads may occur simultaneously.
- // Note: manually sync avoids locking per-item (e.g. Collections.synchronizedMap)
- for (int i = 0; i < dest.size(); i++) {
- mKeyMap.put(dest.get(i), mSource.getKey(source.get(i)));
- }
- }
- return dest;
- }
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams<K> params,
- final @NonNull LoadInitialCallback<B> callback) {
- mSource.loadInitial(params, new LoadInitialCallback<A>() {
- @Override
- public void onResult(@NonNull List<A> data, int position, int totalCount) {
- callback.onResult(convertWithStashedKeys(data), position, totalCount);
- }
-
- @Override
- public void onResult(@NonNull List<A> data) {
- callback.onResult(convertWithStashedKeys(data));
- }
- });
- }
-
- @Override
- public void loadAfter(@NonNull LoadParams<K> params,
- final @NonNull LoadCallback<B> callback) {
- mSource.loadAfter(params, new LoadCallback<A>() {
- @Override
- public void onResult(@NonNull List<A> data) {
- callback.onResult(convertWithStashedKeys(data));
- }
- });
- }
-
- @Override
- public void loadBefore(@NonNull LoadParams<K> params,
- final @NonNull LoadCallback<B> callback) {
- mSource.loadBefore(params, new LoadCallback<A>() {
- @Override
- public void onResult(@NonNull List<A> data) {
- callback.onResult(convertWithStashedKeys(data));
- }
- });
- }
-
- @NonNull
- @Override
- public K getKey(@NonNull B item) {
- synchronized (mKeyMap) {
- return mKeyMap.get(item);
- }
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java
deleted file mode 100644
index e6fa274..0000000
--- a/paging/common/src/main/java/android/arch/paging/WrapperPageKeyedDataSource.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2018 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.List;
-
-class WrapperPageKeyedDataSource<K, A, B> extends PageKeyedDataSource<K, B> {
- private final PageKeyedDataSource<K, A> mSource;
- private final Function<List<A>, List<B>> mListFunction;
- private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- removeCallback();
- }
- };
-
- WrapperPageKeyedDataSource(PageKeyedDataSource<K, A> source,
- Function<List<A>, List<B>> listFunction) {
- mSource = source;
- mListFunction = listFunction;
- mSource.addInvalidatedCallback(mInvalidatedCallback);
- }
-
- private void removeCallback() {
- mSource.removeInvalidatedCallback(mInvalidatedCallback);
- }
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams<K> params,
- final @NonNull LoadInitialCallback<K, B> callback) {
- mSource.loadInitial(params, new LoadInitialCallback<K, A>() {
- @Override
- public void onResult(@NonNull List<A> data, int position, int totalCount,
- @Nullable K previousPageKey, @Nullable K nextPageKey) {
- callback.onResult(convert(mListFunction, data), position, totalCount,
- previousPageKey, nextPageKey);
- }
-
- @Override
- public void onResult(@NonNull List<A> data, @Nullable K previousPageKey,
- @Nullable K nextPageKey) {
- callback.onResult(convert(mListFunction, data), previousPageKey, nextPageKey);
- }
- });
- }
-
- @Override
- public void loadBefore(@NonNull LoadParams<K> params,
- final @NonNull LoadCallback<K, B> callback) {
- mSource.loadBefore(params, new LoadCallback<K, A>() {
- @Override
- public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
- callback.onResult(convert(mListFunction, data), adjacentPageKey);
- }
- });
- }
-
- @Override
- public void loadAfter(@NonNull LoadParams<K> params,
- final @NonNull LoadCallback<K, B> callback) {
- mSource.loadAfter(params, new LoadCallback<K, A>() {
- @Override
- public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
- callback.onResult(convert(mListFunction, data), adjacentPageKey);
- }
- });
- }
-}
diff --git a/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java b/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java
deleted file mode 100644
index 0626f87..0000000
--- a/paging/common/src/main/java/android/arch/paging/WrapperPositionalDataSource.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2018 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.paging;
-
-import android.arch.core.util.Function;
-import android.support.annotation.NonNull;
-
-import java.util.List;
-
-class WrapperPositionalDataSource<A, B> extends PositionalDataSource<B> {
- private final PositionalDataSource<A> mSource;
- private final Function<List<A>, List<B>> mListFunction;
-
- private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- removeCallback();
- }
- };
-
- WrapperPositionalDataSource(PositionalDataSource<A> source,
- Function<List<A>, List<B>> listFunction) {
- mSource = source;
- mListFunction = listFunction;
- mSource.addInvalidatedCallback(mInvalidatedCallback);
- }
-
- private void removeCallback() {
- mSource.removeInvalidatedCallback(mInvalidatedCallback);
- }
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams params,
- final @NonNull LoadInitialCallback<B> callback) {
- mSource.loadInitial(params, new LoadInitialCallback<A>() {
- @Override
- public void onResult(@NonNull List<A> data, int position, int totalCount) {
- callback.onResult(convert(mListFunction, data), position, totalCount);
- }
-
- @Override
- public void onResult(@NonNull List<A> data, int position) {
- callback.onResult(convert(mListFunction, data), position);
- }
- });
- }
-
- @Override
- public void loadRange(@NonNull LoadRangeParams params,
- final @NonNull LoadRangeCallback<B> callback) {
- mSource.loadRange(params, new LoadRangeCallback<A>() {
- @Override
- public void onResult(@NonNull List<A> data) {
- callback.onResult(convert(mListFunction, data));
- }
- });
- }
-}
diff --git a/paging/common/src/main/java/androidx/paging/ContiguousDataSource.java b/paging/common/src/main/java/androidx/paging/ContiguousDataSource.java
new file mode 100644
index 0000000..dc95d66
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/ContiguousDataSource.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Executor;
+
+abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
+ @Override
+ boolean isContiguous() {
+ return true;
+ }
+
+ abstract void dispatchLoadInitial(
+ @Nullable Key key,
+ int initialLoadSize,
+ int pageSize,
+ boolean enablePlaceholders,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver);
+
+ abstract void dispatchLoadAfter(
+ int currentEndIndex,
+ @NonNull Value currentEndItem,
+ int pageSize,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver);
+
+ abstract void dispatchLoadBefore(
+ int currentBeginIndex,
+ @NonNull Value currentBeginItem,
+ int pageSize,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver);
+
+ /**
+ * Get the key from either the position, or item, or null if position/item invalid.
+ * <p>
+ * Position may not match passed item's position - if trying to query the key from a position
+ * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed.
+ */
+ abstract Key getKey(int position, Value item);
+}
diff --git a/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java b/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java
new file mode 100644
index 0000000..1c39a8b
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/ContiguousPagedList.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback {
+ private final ContiguousDataSource<K, V> mDataSource;
+ private boolean mPrependWorkerRunning = false;
+ private boolean mAppendWorkerRunning = false;
+
+ private int mPrependItemsRequested = 0;
+ private int mAppendItemsRequested = 0;
+
+ private PageResult.Receiver<V> mReceiver = new PageResult.Receiver<V>() {
+ // Creation thread for initial synchronous load, otherwise main thread
+ // Safe to access main thread only state - no other thread has reference during construction
+ @AnyThread
+ @Override
+ public void onPageResult(@PageResult.ResultType int resultType,
+ @NonNull PageResult<V> pageResult) {
+ if (pageResult.isInvalid()) {
+ detach();
+ return;
+ }
+
+ if (isDetached()) {
+ // No op, have detached
+ return;
+ }
+
+ List<V> page = pageResult.page;
+ if (resultType == PageResult.INIT) {
+ mStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls,
+ pageResult.positionOffset, ContiguousPagedList.this);
+ if (mLastLoad == LAST_LOAD_UNSPECIFIED) {
+ // Because the ContiguousPagedList wasn't initialized with a last load position,
+ // initialize it to the middle of the initial load
+ mLastLoad =
+ pageResult.leadingNulls + pageResult.positionOffset + page.size() / 2;
+ }
+ } else if (resultType == PageResult.APPEND) {
+ mStorage.appendPage(page, ContiguousPagedList.this);
+ } else if (resultType == PageResult.PREPEND) {
+ mStorage.prependPage(page, ContiguousPagedList.this);
+ } else {
+ throw new IllegalArgumentException("unexpected resultType " + resultType);
+ }
+
+
+ if (mBoundaryCallback != null) {
+ boolean deferEmpty = mStorage.size() == 0;
+ boolean deferBegin = !deferEmpty
+ && resultType == PageResult.PREPEND
+ && pageResult.page.size() == 0;
+ boolean deferEnd = !deferEmpty
+ && resultType == PageResult.APPEND
+ && pageResult.page.size() == 0;
+ deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
+ }
+ }
+ };
+
+ static final int LAST_LOAD_UNSPECIFIED = -1;
+
+ ContiguousPagedList(
+ @NonNull ContiguousDataSource<K, V> dataSource,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @Nullable BoundaryCallback<V> boundaryCallback,
+ @NonNull Config config,
+ final @Nullable K key,
+ int lastLoad) {
+ super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
+ boundaryCallback, config);
+ mDataSource = dataSource;
+ mLastLoad = lastLoad;
+
+ if (mDataSource.isInvalid()) {
+ detach();
+ } else {
+ mDataSource.dispatchLoadInitial(key,
+ mConfig.initialLoadSizeHint,
+ mConfig.pageSize,
+ mConfig.enablePlaceholders,
+ mMainThreadExecutor,
+ mReceiver);
+ }
+ }
+
+ @MainThread
+ @Override
+ void dispatchUpdatesSinceSnapshot(
+ @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) {
+ final PagedStorage<V> snapshot = pagedListSnapshot.mStorage;
+
+ final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended();
+ final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended();
+
+ final int previousTrailing = snapshot.getTrailingNullCount();
+ final int previousLeading = snapshot.getLeadingNullCount();
+
+ // Validate that the snapshot looks like a previous version of this list - if it's not,
+ // we can't be sure we'll dispatch callbacks safely
+ if (snapshot.isEmpty()
+ || newlyAppended < 0
+ || newlyPrepended < 0
+ || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0)
+ || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0)
+ || (mStorage.getStorageCount()
+ != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) {
+ throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
+ + " to be a snapshot of this PagedList");
+ }
+
+ if (newlyAppended != 0) {
+ final int changedCount = Math.min(previousTrailing, newlyAppended);
+ final int addedCount = newlyAppended - changedCount;
+
+ final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount();
+ if (changedCount != 0) {
+ callback.onChanged(endPosition, changedCount);
+ }
+ if (addedCount != 0) {
+ callback.onInserted(endPosition + changedCount, addedCount);
+ }
+ }
+ if (newlyPrepended != 0) {
+ final int changedCount = Math.min(previousLeading, newlyPrepended);
+ final int addedCount = newlyPrepended - changedCount;
+
+ if (changedCount != 0) {
+ callback.onChanged(previousLeading, changedCount);
+ }
+ if (addedCount != 0) {
+ callback.onInserted(0, addedCount);
+ }
+ }
+ }
+
+ @MainThread
+ @Override
+ protected void loadAroundInternal(int index) {
+ int prependItems = mConfig.prefetchDistance - (index - mStorage.getLeadingNullCount());
+ int appendItems = index + mConfig.prefetchDistance
+ - (mStorage.getLeadingNullCount() + mStorage.getStorageCount());
+
+ mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
+ if (mPrependItemsRequested > 0) {
+ schedulePrepend();
+ }
+
+ mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
+ if (mAppendItemsRequested > 0) {
+ scheduleAppend();
+ }
+ }
+
+ @MainThread
+ private void schedulePrepend() {
+ if (mPrependWorkerRunning) {
+ return;
+ }
+ mPrependWorkerRunning = true;
+
+ final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset();
+
+ // safe to access first item here - mStorage can't be empty if we're prepending
+ final V item = mStorage.getFirstLoadedItem();
+ mBackgroundThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (isDetached()) {
+ return;
+ }
+ if (mDataSource.isInvalid()) {
+ detach();
+ } else {
+ mDataSource.dispatchLoadBefore(position, item, mConfig.pageSize,
+ mMainThreadExecutor, mReceiver);
+ }
+
+ }
+ });
+ }
+
+ @MainThread
+ private void scheduleAppend() {
+ if (mAppendWorkerRunning) {
+ return;
+ }
+ mAppendWorkerRunning = true;
+
+ final int position = mStorage.getLeadingNullCount()
+ + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();
+
+ // safe to access first item here - mStorage can't be empty if we're appending
+ final V item = mStorage.getLastLoadedItem();
+ mBackgroundThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (isDetached()) {
+ return;
+ }
+ if (mDataSource.isInvalid()) {
+ detach();
+ } else {
+ mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
+ mMainThreadExecutor, mReceiver);
+ }
+ }
+ });
+ }
+
+ @Override
+ boolean isContiguous() {
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public DataSource<?, V> getDataSource() {
+ return mDataSource;
+ }
+
+ @Nullable
+ @Override
+ public Object getLastKey() {
+ return mDataSource.getKey(mLastLoad, mLastItem);
+ }
+
+ @MainThread
+ @Override
+ public void onInitialized(int count) {
+ notifyInserted(0, count);
+ }
+
+ @MainThread
+ @Override
+ public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) {
+ // consider whether to post more work, now that a page is fully prepended
+ mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount;
+ mPrependWorkerRunning = false;
+ if (mPrependItemsRequested > 0) {
+ // not done prepending, keep going
+ schedulePrepend();
+ }
+
+ // finally dispatch callbacks, after prepend may have already been scheduled
+ notifyChanged(leadingNulls, changedCount);
+ notifyInserted(0, addedCount);
+
+ offsetBoundaryAccessIndices(addedCount);
+ }
+
+ @MainThread
+ @Override
+ public void onPageAppended(int endPosition, int changedCount, int addedCount) {
+ // consider whether to post more work, now that a page is fully appended
+
+ mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
+ mAppendWorkerRunning = false;
+ if (mAppendItemsRequested > 0) {
+ // not done appending, keep going
+ scheduleAppend();
+ }
+
+ // finally dispatch callbacks, after append may have already been scheduled
+ notifyChanged(endPosition, changedCount);
+ notifyInserted(endPosition + changedCount, addedCount);
+ }
+
+ @MainThread
+ @Override
+ public void onPagePlaceholderInserted(int pageIndex) {
+ throw new IllegalStateException("Tiled callback on ContiguousPagedList");
+ }
+
+ @MainThread
+ @Override
+ public void onPageInserted(int start, int count) {
+ throw new IllegalStateException("Tiled callback on ContiguousPagedList");
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/DataSource.java b/paging/common/src/main/java/androidx/paging/DataSource.java
new file mode 100644
index 0000000..55cd0a9
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/DataSource.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.util.Function;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Base class for loading pages of snapshot data into a {@link PagedList}.
+ * <p>
+ * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
+ * it loads more data, but the data loaded cannot be updated. If the underlying data set is
+ * modified, a new PagedList / DataSource pair must be created to represent the new data.
+ * <h4>Loading Pages</h4>
+ * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
+ * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
+ * <p>
+ * To control how and when a PagedList queries data from its DataSource, see
+ * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
+ * <h4>Updating Paged Data</h4>
+ * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
+ * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
+ * content update occurs. A DataSource must detect that it cannot continue loading its
+ * snapshot (for instance, when Database query notices a table being invalidated), and call
+ * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
+ * the new state of the Database query.
+ * <p>
+ * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
+ * PagedList. For example, loading from network when the network's paging API doesn't provide
+ * updates.
+ * <p>
+ * To page in data from a source that does provide updates, you can create a
+ * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
+ * data set occurs that makes the current snapshot invalid. For example, when paging a query from
+ * the Database, and the table being queried inserts or removes items. You can also use a
+ * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
+ * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
+ * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
+ * DataSource.
+ * <p>
+ * If you have more granular update signals, such as a network API signaling an update to a single
+ * item in the list, it's recommended to load data from network into memory. Then present that
+ * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
+ * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
+ * snapshot can be created.
+ * <h4>Implementing a DataSource</h4>
+ * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
+ * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
+ * <p>
+ * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
+ * example a network response that returns some items, and a next/previous page links.
+ * <p>
+ * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
+ * {@code N}. For example, if requesting the backend for the next comments in the list
+ * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
+ * from a name-sorted database query requires the name and unique ID of the previous.
+ * <p>
+ * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
+ * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
+ * PositionalDataSource is required to respect page size for efficient tiling. If you want to
+ * override page size (e.g. when network page size constraints are only known at runtime), use one
+ * of the other DataSource classes.
+ * <p>
+ * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
+ * return {@code null} items in lists that it loads. This is so that users of the PagedList
+ * can differentiate unloaded placeholder items from content that has been paged in.
+ *
+ * @param <Key> Input used to trigger initial load from the DataSource. Often an Integer position.
+ * @param <Value> Value type loaded by the DataSource.
+ */
+@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
+public abstract class DataSource<Key, Value> {
+ /**
+ * Factory for DataSources.
+ * <p>
+ * Data-loading systems of an application or library can implement this interface to allow
+ * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
+ * DataSource.Factory for a given SQL query:
+ *
+ * <pre>
+ * {@literal @}Dao
+ * interface UserDao {
+ * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ * public abstract DataSource.Factory<Integer, User> usersByLastName();
+ * }
+ * </pre>
+ * In the above sample, {@code Integer} is used because it is the {@code Key} type of
+ * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
+ * page a large query with a PositionalDataSource.
+ *
+ * @param <Key> Key identifying items in DataSource.
+ * @param <Value> Type of items in the list loaded by the DataSources.
+ */
+ public abstract static class Factory<Key, Value> {
+ /**
+ * Create a DataSource.
+ * <p>
+ * The DataSource should invalidate itself if the snapshot is no longer valid. If a
+ * DataSource becomes invalid, the only way to query more data is to create a new DataSource
+ * from the Factory.
+ * <p>
+ * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
+ * when the current DataSource is invalidated, and pass the new PagedList through the
+ * {@code LiveData<PagedList>} to observers.
+ *
+ * @return the new DataSource.
+ */
+ public abstract DataSource<Key, Value> create();
+
+ /**
+ * Applies the given function to each value emitted by DataSources produced by this Factory.
+ * <p>
+ * Same as {@link #mapByPage(Function)}, but operates on individual items.
+ *
+ * @param function Function that runs on each loaded item, returning items of a potentially
+ * new type.
+ * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
+ *
+ * @return A new DataSource.Factory, which transforms items using the given function.
+ *
+ * @see #mapByPage(Function)
+ * @see DataSource#map(Function)
+ * @see DataSource#mapByPage(Function)
+ */
+ @NonNull
+ public <ToValue> DataSource.Factory<Key, ToValue> map(
+ @NonNull Function<Value, ToValue> function) {
+ return mapByPage(createListFunction(function));
+ }
+
+ /**
+ * Applies the given function to each value emitted by DataSources produced by this Factory.
+ * <p>
+ * Same as {@link #map(Function)}, but allows for batch conversions.
+ *
+ * @param function Function that runs on each loaded page, returning items of a potentially
+ * new type.
+ * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
+ *
+ * @return A new DataSource.Factory, which transforms items using the given function.
+ *
+ * @see #map(Function)
+ * @see DataSource#map(Function)
+ * @see DataSource#mapByPage(Function)
+ */
+ @NonNull
+ public <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
+ @NonNull final Function<List<Value>, List<ToValue>> function) {
+ return new Factory<Key, ToValue>() {
+ @Override
+ public DataSource<Key, ToValue> create() {
+ return Factory.this.create().mapByPage(function);
+ }
+ };
+ }
+ }
+
+ @NonNull
+ static <X, Y> Function<List<X>, List<Y>> createListFunction(
+ final @NonNull Function<X, Y> innerFunc) {
+ return new Function<List<X>, List<Y>>() {
+ @Override
+ public List<Y> apply(@NonNull List<X> source) {
+ List<Y> out = new ArrayList<>(source.size());
+ for (int i = 0; i < source.size(); i++) {
+ out.add(innerFunc.apply(source.get(i)));
+ }
+ return out;
+ }
+ };
+ }
+
+ static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
+ List<B> dest = function.apply(source);
+ if (dest.size() != source.size()) {
+ throw new IllegalStateException("Invalid Function " + function
+ + " changed return size. This is not supported.");
+ }
+ return dest;
+ }
+
+ // Since we currently rely on implementation details of two implementations,
+ // prevent external subclassing, except through exposed subclasses
+ DataSource() {
+ }
+
+ /**
+ * Applies the given function to each value emitted by the DataSource.
+ * <p>
+ * Same as {@link #map(Function)}, but allows for batch conversions.
+ *
+ * @param function Function that runs on each loaded page, returning items of a potentially
+ * new type.
+ * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
+ *
+ * @return A new DataSource, which transforms items using the given function.
+ *
+ * @see #map(Function)
+ * @see DataSource.Factory#map(Function)
+ * @see DataSource.Factory#mapByPage(Function)
+ */
+ @NonNull
+ public abstract <ToValue> DataSource<Key, ToValue> mapByPage(
+ @NonNull Function<List<Value>, List<ToValue>> function);
+
+ /**
+ * Applies the given function to each value emitted by the DataSource.
+ * <p>
+ * Same as {@link #mapByPage(Function)}, but operates on individual items.
+ *
+ * @param function Function that runs on each loaded item, returning items of a potentially
+ * new type.
+ * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
+ *
+ * @return A new DataSource, which transforms items using the given function.
+ *
+ * @see #mapByPage(Function)
+ * @see DataSource.Factory#map(Function)
+ * @see DataSource.Factory#mapByPage(Function)
+ */
+ @NonNull
+ public abstract <ToValue> DataSource<Key, ToValue> map(
+ @NonNull Function<Value, ToValue> function);
+
+ /**
+ * Returns true if the data source guaranteed to produce a contiguous set of items,
+ * never producing gaps.
+ */
+ abstract boolean isContiguous();
+
+ static class LoadCallbackHelper<T> {
+ static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
+ if (position < 0) {
+ throw new IllegalArgumentException("Position must be non-negative");
+ }
+ if (data.size() + position > totalCount) {
+ throw new IllegalArgumentException(
+ "List size + position too large, last item in list beyond totalCount.");
+ }
+ if (data.size() == 0 && totalCount > 0) {
+ throw new IllegalArgumentException(
+ "Initial result cannot be empty if items are present in data set.");
+ }
+ }
+
+ @PageResult.ResultType
+ final int mResultType;
+ private final DataSource mDataSource;
+ private final PageResult.Receiver<T> mReceiver;
+
+ // mSignalLock protects mPostExecutor, and mHasSignalled
+ private final Object mSignalLock = new Object();
+ private Executor mPostExecutor = null;
+ private boolean mHasSignalled = false;
+
+ LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
+ @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
+ mDataSource = dataSource;
+ mResultType = resultType;
+ mPostExecutor = mainThreadExecutor;
+ mReceiver = receiver;
+ }
+
+ void setPostExecutor(Executor postExecutor) {
+ synchronized (mSignalLock) {
+ mPostExecutor = postExecutor;
+ }
+ }
+
+ /**
+ * Call before verifying args, or dispatching actul results
+ *
+ * @return true if DataSource was invalid, and invalid result dispatched
+ */
+ boolean dispatchInvalidResultIfInvalid() {
+ if (mDataSource.isInvalid()) {
+ dispatchResultToReceiver(PageResult.<T>getInvalidResult());
+ return true;
+ }
+ return false;
+ }
+
+ void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
+ Executor executor;
+ synchronized (mSignalLock) {
+ if (mHasSignalled) {
+ throw new IllegalStateException(
+ "callback.onResult already called, cannot call again.");
+ }
+ mHasSignalled = true;
+ executor = mPostExecutor;
+ }
+
+ if (executor != null) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mReceiver.onPageResult(mResultType, result);
+ }
+ });
+ } else {
+ mReceiver.onPageResult(mResultType, result);
+ }
+ }
+ }
+
+ /**
+ * Invalidation callback for DataSource.
+ * <p>
+ * Used to signal when a DataSource a data source has become invalid, and that a new data source
+ * is needed to continue loading data.
+ */
+ public interface InvalidatedCallback {
+ /**
+ * Called when the data backing the list has become invalid. This callback is typically used
+ * to signal that a new data source is needed.
+ * <p>
+ * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
+ * for the data source to invalidate itself during its load methods, or for an outside
+ * source to invalidate it.
+ */
+ @AnyThread
+ void onInvalidated();
+ }
+
+ private AtomicBoolean mInvalid = new AtomicBoolean(false);
+
+ private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
+ new CopyOnWriteArrayList<>();
+
+ /**
+ * Add a callback to invoke when the DataSource is first invalidated.
+ * <p>
+ * Once invalidated, a data source will not become valid again.
+ * <p>
+ * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
+ * is called, on that thread.
+ *
+ * @param onInvalidatedCallback The callback, will be invoked on thread that
+ * {@link #invalidate()} is called on.
+ */
+ @AnyThread
+ @SuppressWarnings("WeakerAccess")
+ public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mOnInvalidatedCallbacks.add(onInvalidatedCallback);
+ }
+
+ /**
+ * Remove a previously added invalidate callback.
+ *
+ * @param onInvalidatedCallback The previously added callback.
+ */
+ @AnyThread
+ @SuppressWarnings("WeakerAccess")
+ public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
+ }
+
+ /**
+ * Signal the data source to stop loading, and notify its callback.
+ * <p>
+ * If invalidate has already been called, this method does nothing.
+ */
+ @AnyThread
+ public void invalidate() {
+ if (mInvalid.compareAndSet(false, true)) {
+ for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
+ callback.onInvalidated();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the data source is invalid, and can no longer be queried for data.
+ *
+ * @return True if the data source is invalid, and can no longer return data.
+ */
+ @WorkerThread
+ public boolean isInvalid() {
+ return mInvalid.get();
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java
new file mode 100644
index 0000000..5f27710
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/ItemKeyedDataSource.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Incremental data loader for paging keyed content, where loaded content uses previously loaded
+ * items as input to future loads.
+ * <p>
+ * Implement a DataSource using ItemKeyedDataSource if you need to use data from item {@code N - 1}
+ * to load item {@code N}. This is common, for example, in sorted database queries where
+ * attributes of the item such just before the next query define how to execute it.
+ * <p>
+ * The {@code InMemoryByItemRepository} in the
+ * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
+ * shows how to implement a network ItemKeyedDataSource using
+ * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
+ * handling swipe-to-refresh, network errors, and retry.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ * @param <Value> Type of items being loaded by the DataSource.
+ */
+public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
+
+ /**
+ * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadInitialParams<Key> {
+ /**
+ * Load items around this key, or at the beginning of the data set if {@code null} is
+ * passed.
+ * <p>
+ * Note that this key is generally a hint, and may be ignored if you want to always load
+ * from the beginning.
+ */
+ @Nullable
+ public final Key requestedInitialKey;
+
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Note that this may be larger than available data.
+ */
+ public final int requestedLoadSize;
+
+ /**
+ * Defines whether placeholders are enabled, and whether the total count passed to
+ * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
+ */
+ public final boolean placeholdersEnabled;
+
+
+ public LoadInitialParams(@Nullable Key requestedInitialKey, int requestedLoadSize,
+ boolean placeholdersEnabled) {
+ this.requestedInitialKey = requestedInitialKey;
+ this.requestedLoadSize = requestedLoadSize;
+ this.placeholdersEnabled = placeholdersEnabled;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)}
+ * and {@link #loadAfter(LoadParams, LoadCallback)}.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadParams<Key> {
+ /**
+ * Load items before/after this key.
+ * <p>
+ * Returned data must begin directly adjacent to this position.
+ */
+ public final Key key;
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Returned page can be of this size, but it may be altered if that is easier, e.g. a
+ * network data source where the backend defines page size.
+ */
+ public final int requestedLoadSize;
+
+ public LoadParams(Key key, int requestedLoadSize) {
+ this.key = key;
+ this.requestedLoadSize = requestedLoadSize;
+ }
+ }
+
+ /**
+ * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
+ * to return data and, optionally, position/count information.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * If you can compute the number of items in the data set before and after the loaded range,
+ * call the three parameter {@link #onResult(List, int, int)} to pass that information. You
+ * can skip passing this information by calling the single parameter {@link #onResult(List)},
+ * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is
+ * {@code false}, so the positioning information will be ignored.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Value> Type of items being loaded.
+ */
+ public abstract static class LoadInitialCallback<Value> extends LoadCallback<Value> {
+ /**
+ * Called to pass initial load state from a DataSource.
+ * <p>
+ * Call this method from your DataSource's {@code loadInitial} function to return data,
+ * and inform how many placeholders should be shown before and after. If counting is cheap
+ * to compute (for example, if a network load returns the information regardless), it's
+ * recommended to pass data back through this method.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the DataSource. If this is empty, the DataSource
+ * is treated as empty, and no further loads will occur.
+ * @param position Position of the item at the front of the list. If there are {@code N}
+ * items before the items in data that can be loaded from this DataSource,
+ * pass {@code N}.
+ * @param totalCount Total number of items that may be returned from this DataSource.
+ * Includes the number in the initial {@code data} parameter
+ * as well as any items that can be loaded in front or behind of
+ * {@code data}.
+ */
+ public abstract void onResult(@NonNull List<Value> data, int position, int totalCount);
+ }
+
+
+ /**
+ * Callback for ItemKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)}
+ * and {@link #loadAfter(LoadParams, LoadCallback)} to return data.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Value> Type of items being loaded.
+ */
+ public abstract static class LoadCallback<Value> {
+ /**
+ * Called to pass loaded data from a DataSource.
+ * <p>
+ * Call this method from your ItemKeyedDataSource's
+ * {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
+ * <p>
+ * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
+ * initialize without counting available data, or supporting placeholders.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the ItemKeyedDataSource.
+ */
+ public abstract void onResult(@NonNull List<Value> data);
+ }
+
+ static class LoadInitialCallbackImpl<Value> extends LoadInitialCallback<Value> {
+ final LoadCallbackHelper<Value> mCallbackHelper;
+ private final boolean mCountingEnabled;
+ LoadInitialCallbackImpl(@NonNull ItemKeyedDataSource dataSource, boolean countingEnabled,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
+ mCountingEnabled = countingEnabled;
+ }
+
+ @Override
+ public void onResult(@NonNull List<Value> data, int position, int totalCount) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
+
+ int trailingUnloadedCount = totalCount - position - data.size();
+ if (mCountingEnabled) {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
+ data, position, trailingUnloadedCount, 0));
+ } else {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
+ }
+ }
+ }
+
+ @Override
+ public void onResult(@NonNull List<Value> data) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ static class LoadCallbackImpl<Value> extends LoadCallback<Value> {
+ final LoadCallbackHelper<Value> mCallbackHelper;
+
+ LoadCallbackImpl(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type,
+ @Nullable Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ mCallbackHelper = new LoadCallbackHelper<>(
+ dataSource, type, mainThreadExecutor, receiver);
+ }
+
+ @Override
+ public void onResult(@NonNull List<Value> data) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ final Key getKey(int position, Value item) {
+ if (item == null) {
+ return null;
+ }
+
+ return getKey(item);
+ }
+
+ @Override
+ final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
+ boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ LoadInitialCallbackImpl<Value> callback =
+ new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
+ loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
+
+ // If initialLoad's callback is not called within the body, we force any following calls
+ // to post to the UI thread. This constructor may be run on a background thread, but
+ // after constructor, mutation must happen on UI thread.
+ callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
+ }
+
+ @Override
+ final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),
+ new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
+ }
+
+ @Override
+ final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),
+ new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
+ }
+
+ /**
+ * Load initial data.
+ * <p>
+ * This method is called first to initialize a PagedList with data. If it's possible to count
+ * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
+ * the callback via the three-parameter
+ * {@link LoadInitialCallback#onResult(List, int, int)}. This enables PagedLists
+ * presenting data from this source to display placeholders to represent unloaded items.
+ * <p>
+ * {@link LoadInitialParams#requestedInitialKey} and {@link LoadInitialParams#requestedLoadSize}
+ * are hints, not requirements, so they may be altered or ignored. Note that ignoring the
+ * {@code requestedInitialKey} can prevent subsequent PagedList/DataSource pairs from
+ * initializing at the same location. If your data source never invalidates (for example,
+ * loading from the network without the network ever signalling that old data must be reloaded),
+ * it's fine to ignore the {@code initialLoadKey} and always start from the beginning of the
+ * data set.
+ *
+ * @param params Parameters for initial load, including initial key and requested size.
+ * @param callback Callback that receives initial load data.
+ */
+ public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
+ @NonNull LoadInitialCallback<Value> callback);
+
+ /**
+ * Load list data after the key specified in {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * Data may be passed synchronously during the loadAfter method, or deferred and called at a
+ * later time. Further loads going down will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key to load after, and requested size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadAfter(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Value> callback);
+
+ /**
+ * Load list data before the key specified in {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * <p class="note"><strong>Note:</strong> Data returned will be prepended just before the key
+ * passed, so if you vary size, ensure that the last item is adjacent to the passed key.
+ * <p>
+ * Data may be passed synchronously during the loadBefore method, or deferred and called at a
+ * later time. Further loads going up will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key to load before, and requested size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadBefore(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Value> callback);
+
+ /**
+ * Return a key associated with the given item.
+ * <p>
+ * If your ItemKeyedDataSource 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(LoadParams, LoadCallback)} or
+ * {@link #loadAfter(LoadParams, LoadCallback)} 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
+ public abstract Key getKey(@NonNull Value item);
+
+ @NonNull
+ @Override
+ public final <ToValue> ItemKeyedDataSource<Key, ToValue> mapByPage(
+ @NonNull Function<List<Value>, List<ToValue>> function) {
+ return new WrapperItemKeyedDataSource<>(this, function);
+ }
+
+ @NonNull
+ @Override
+ public final <ToValue> ItemKeyedDataSource<Key, ToValue> map(
+ @NonNull Function<Value, ToValue> function) {
+ return mapByPage(createListFunction(function));
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/ListDataSource.java b/paging/common/src/main/java/androidx/paging/ListDataSource.java
new file mode 100644
index 0000000..481d5f0
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/ListDataSource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class ListDataSource<T> extends PositionalDataSource<T> {
+ private final List<T> mList;
+
+ public ListDataSource(List<T> list) {
+ mList = new ArrayList<>(list);
+ }
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
+ final int totalCount = mList.size();
+
+ final int position = computeInitialLoadPosition(params, totalCount);
+ final int loadSize = computeInitialLoadSize(params, position, totalCount);
+
+ // for simplicity, we could return everything immediately,
+ // but we tile here since it's expected behavior
+ List<T> sublist = mList.subList(position, position + loadSize);
+ callback.onResult(sublist, position, totalCount);
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback) {
+ callback.onResult(mList.subList(params.startPosition,
+ params.startPosition + params.loadSize));
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
new file mode 100644
index 0000000..db6dddf
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/PageKeyedDataSource.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Incremental data loader for page-keyed content, where requests return keys for next/previous
+ * pages.
+ * <p>
+ * Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1}
+ * to load page {@code N}. This is common, for example, in network APIs that include a next/previous
+ * link or key with each page load.
+ * <p>
+ * The {@code InMemoryByPageRepository} in the
+ * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
+ * shows how to implement a network PageKeyedDataSource using
+ * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
+ * handling swipe-to-refresh, network errors, and retry.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ * @param <Value> Type of items being loaded by the DataSource.
+ */
+public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
+ private final Object mKeyLock = new Object();
+
+ @Nullable
+ @GuardedBy("mKeyLock")
+ private Key mNextKey = null;
+
+ @Nullable
+ @GuardedBy("mKeyLock")
+ private Key mPreviousKey = null;
+
+ private void initKeys(@Nullable Key previousKey, @Nullable Key nextKey) {
+ synchronized (mKeyLock) {
+ mPreviousKey = previousKey;
+ mNextKey = nextKey;
+ }
+ }
+
+ private void setPreviousKey(@Nullable Key previousKey) {
+ synchronized (mKeyLock) {
+ mPreviousKey = previousKey;
+ }
+ }
+
+ private void setNextKey(@Nullable Key nextKey) {
+ synchronized (mKeyLock) {
+ mNextKey = nextKey;
+ }
+ }
+
+ private @Nullable Key getPreviousKey() {
+ synchronized (mKeyLock) {
+ return mPreviousKey;
+ }
+ }
+
+ private @Nullable Key getNextKey() {
+ synchronized (mKeyLock) {
+ return mNextKey;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param <Key> Type of data used to query pages.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadInitialParams<Key> {
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Note that this may be larger than available data.
+ */
+ public final int requestedLoadSize;
+
+ /**
+ * Defines whether placeholders are enabled, and whether the total count passed to
+ * {@link LoadInitialCallback#onResult(List, int, int, Key, Key)} will be ignored.
+ */
+ public final boolean placeholdersEnabled;
+
+
+ public LoadInitialParams(int requestedLoadSize, boolean placeholdersEnabled) {
+ this.requestedLoadSize = requestedLoadSize;
+ this.placeholdersEnabled = placeholdersEnabled;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)}.
+ *
+ * @param <Key> Type of data used to query pages.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadParams<Key> {
+ /**
+ * Load items before/after this key.
+ * <p>
+ * Returned data must begin directly adjacent to this position.
+ */
+ public final Key key;
+
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Returned page can be of this size, but it may be altered if that is easier, e.g. a
+ * network data source where the backend defines page size.
+ */
+ public final int requestedLoadSize;
+
+ public LoadParams(Key key, int requestedLoadSize) {
+ this.key = key;
+ this.requestedLoadSize = requestedLoadSize;
+ }
+ }
+
+ /**
+ * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
+ * to return data and, optionally, position/count information.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * If you can compute the number of items in the data set before and after the loaded range,
+ * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that
+ * information. You can skip passing this information by calling the three parameter
+ * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if
+ * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning
+ * information will be ignored.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Key> Type of data used to query pages.
+ * @param <Value> Type of items being loaded.
+ */
+ public abstract static class LoadInitialCallback<Key, Value> {
+ /**
+ * Called to pass initial load state from a DataSource.
+ * <p>
+ * Call this method from your DataSource's {@code loadInitial} function to return data,
+ * and inform how many placeholders should be shown before and after. If counting is cheap
+ * to compute (for example, if a network load returns the information regardless), it's
+ * recommended to pass data back through this method.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the DataSource. If this is empty, the DataSource
+ * is treated as empty, and no further loads will occur.
+ * @param position Position of the item at the front of the list. If there are {@code N}
+ * items before the items in data that can be loaded from this DataSource,
+ * pass {@code N}.
+ * @param totalCount Total number of items that may be returned from this DataSource.
+ * Includes the number in the initial {@code data} parameter
+ * as well as any items that can be loaded in front or behind of
+ * {@code data}.
+ */
+ public abstract void onResult(@NonNull List<Value> data, int position, int totalCount,
+ @Nullable Key previousPageKey, @Nullable Key nextPageKey);
+
+ /**
+ * Called to pass loaded data from a DataSource.
+ * <p>
+ * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
+ * initialize without counting available data, or supporting placeholders.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the PageKeyedDataSource.
+ * @param previousPageKey Key for page before the initial load result, or {@code null} if no
+ * more data can be loaded before.
+ * @param nextPageKey Key for page after the initial load result, or {@code null} if no
+ * more data can be loaded after.
+ */
+ public abstract void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
+ @Nullable Key nextPageKey);
+ }
+
+ /**
+ * Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} to return data.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Key> Type of data used to query pages.
+ * @param <Value> Type of items being loaded.
+ */
+ public abstract static class LoadCallback<Key, Value> {
+
+ /**
+ * Called to pass loaded data from a DataSource.
+ * <p>
+ * Call this method from your PageKeyedDataSource's
+ * {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ * <p>
+ * Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've
+ * loaded a page in {@link #loadBefore(LoadParams, LoadCallback)}, pass the key for the
+ * previous page, or {@code null} if the loaded page is the first. If in
+ * {@link #loadAfter(LoadParams, LoadCallback)}, pass the key for the next page, or
+ * {@code null} if the loaded page is the last.
+ *
+ * @param data List of items loaded from the PageKeyedDataSource.
+ * @param adjacentPageKey Key for subsequent page load (previous page in {@link #loadBefore}
+ * / next page in {@link #loadAfter}), or {@code null} if there are
+ * no more pages to load in the current load direction.
+ */
+ public abstract void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey);
+ }
+
+ static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {
+ final LoadCallbackHelper<Value> mCallbackHelper;
+ private final PageKeyedDataSource<Key, Value> mDataSource;
+ private final boolean mCountingEnabled;
+ LoadInitialCallbackImpl(@NonNull PageKeyedDataSource<Key, Value> dataSource,
+ boolean countingEnabled, @NonNull PageResult.Receiver<Value> receiver) {
+ mCallbackHelper = new LoadCallbackHelper<>(
+ dataSource, PageResult.INIT, null, receiver);
+ mDataSource = dataSource;
+ mCountingEnabled = countingEnabled;
+ }
+
+ @Override
+ public void onResult(@NonNull List<Value> data, int position, int totalCount,
+ @Nullable Key previousPageKey, @Nullable Key nextPageKey) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
+
+ // setup keys before dispatching data, so guaranteed to be ready
+ mDataSource.initKeys(previousPageKey, nextPageKey);
+
+ int trailingUnloadedCount = totalCount - position - data.size();
+ if (mCountingEnabled) {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
+ data, position, trailingUnloadedCount, 0));
+ } else {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
+ }
+ }
+ }
+
+ @Override
+ public void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
+ @Nullable Key nextPageKey) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ mDataSource.initKeys(previousPageKey, nextPageKey);
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ static class LoadCallbackImpl<Key, Value> extends LoadCallback<Key, Value> {
+ final LoadCallbackHelper<Value> mCallbackHelper;
+ private final PageKeyedDataSource<Key, Value> mDataSource;
+ LoadCallbackImpl(@NonNull PageKeyedDataSource<Key, Value> dataSource,
+ @PageResult.ResultType int type, @Nullable Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ mCallbackHelper = new LoadCallbackHelper<>(
+ dataSource, type, mainThreadExecutor, receiver);
+ mDataSource = dataSource;
+ }
+
+ @Override
+ public void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ if (mCallbackHelper.mResultType == PageResult.APPEND) {
+ mDataSource.setNextKey(adjacentPageKey);
+ } else {
+ mDataSource.setPreviousKey(adjacentPageKey);
+ }
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ final Key getKey(int position, Value item) {
+ // don't attempt to persist keys, since we currently don't pass them to initial load
+ return null;
+ }
+
+ @Override
+ final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
+ boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ LoadInitialCallbackImpl<Key, Value> callback =
+ new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
+ loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
+
+ // If initialLoad's callback is not called within the body, we force any following calls
+ // to post to the UI thread. This constructor may be run on a background thread, but
+ // after constructor, mutation must happen on UI thread.
+ callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
+ }
+
+
+ @Override
+ final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ @Nullable Key key = getNextKey();
+ if (key != null) {
+ loadAfter(new LoadParams<>(key, pageSize),
+ new LoadCallbackImpl<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
+ }
+ }
+
+ @Override
+ final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ @Nullable Key key = getPreviousKey();
+ if (key != null) {
+ loadBefore(new LoadParams<>(key, pageSize),
+ new LoadCallbackImpl<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
+ }
+ }
+
+ /**
+ * Load initial data.
+ * <p>
+ * This method is called first to initialize a PagedList with data. If it's possible to count
+ * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
+ * the callback via the three-parameter
+ * {@link LoadInitialCallback#onResult(List, int, int, Object, Object)}. This enables PagedLists
+ * presenting data from this source to display placeholders to represent unloaded items.
+ * <p>
+ * {@link LoadInitialParams#requestedLoadSize} is a hint, not a requirement, so it may be may be
+ * altered or ignored.
+ *
+ * @param params Parameters for initial load, including requested load size.
+ * @param callback Callback that receives initial load data.
+ */
+ public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
+ @NonNull LoadInitialCallback<Key, Value> callback);
+
+ /**
+ * Prepend page with the key specified by {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * Data may be passed synchronously during the load method, or deferred and called at a
+ * later time. Further loads going down will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key for the new page, and requested load
+ * size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadBefore(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Key, Value> callback);
+
+ /**
+ * Append page with the key specified by {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * Data may be passed synchronously during the load method, or deferred and called at a
+ * later time. Further loads going down will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key for the new page, and requested load
+ * size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadAfter(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Key, Value> callback);
+
+ @NonNull
+ @Override
+ public final <ToValue> PageKeyedDataSource<Key, ToValue> mapByPage(
+ @NonNull Function<List<Value>, List<ToValue>> function) {
+ return new WrapperPageKeyedDataSource<>(this, function);
+ }
+
+ @NonNull
+ @Override
+ public final <ToValue> PageKeyedDataSource<Key, ToValue> map(
+ @NonNull Function<Value, ToValue> function) {
+ return mapByPage(createListFunction(function));
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/PageResult.java b/paging/common/src/main/java/androidx/paging/PageResult.java
new file mode 100644
index 0000000..1f7054b
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/PageResult.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.util.Collections;
+import java.util.List;
+
+class PageResult<T> {
+ @SuppressWarnings("unchecked")
+ private static final PageResult INVALID_RESULT =
+ new PageResult(Collections.EMPTY_LIST, 0);
+
+ @SuppressWarnings("unchecked")
+ static <T> PageResult<T> getInvalidResult() {
+ return INVALID_RESULT;
+ }
+
+
+ @Retention(SOURCE)
+ @IntDef({INIT, APPEND, PREPEND, TILE})
+ @interface ResultType {}
+
+ static final int INIT = 0;
+
+ // contiguous results
+ static final int APPEND = 1;
+ static final int PREPEND = 2;
+
+ // non-contiguous, tile result
+ static final int TILE = 3;
+
+ @NonNull
+ public final List<T> page;
+ @SuppressWarnings("WeakerAccess")
+ public final int leadingNulls;
+ @SuppressWarnings("WeakerAccess")
+ public final int trailingNulls;
+ @SuppressWarnings("WeakerAccess")
+ public final int positionOffset;
+
+ PageResult(@NonNull List<T> list, int leadingNulls, int trailingNulls, int positionOffset) {
+ this.page = list;
+ this.leadingNulls = leadingNulls;
+ this.trailingNulls = trailingNulls;
+ this.positionOffset = positionOffset;
+ }
+
+ PageResult(@NonNull List<T> list, int positionOffset) {
+ this.page = list;
+ this.leadingNulls = 0;
+ this.trailingNulls = 0;
+ this.positionOffset = positionOffset;
+ }
+
+ @Override
+ public String toString() {
+ return "Result " + leadingNulls
+ + ", " + page
+ + ", " + trailingNulls
+ + ", offset " + positionOffset;
+ }
+
+ public boolean isInvalid() {
+ return this == INVALID_RESULT;
+ }
+
+ abstract static class Receiver<T> {
+ @MainThread
+ public abstract void onPageResult(@ResultType int type, @NonNull PageResult<T> pageResult);
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/PagedList.java b/paging/common/src/main/java/androidx/paging/PagedList.java
new file mode 100644
index 0000000..2878290
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/PagedList.java
@@ -0,0 +1,1005 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+
+import java.lang.ref.WeakReference;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Lazy loading list that pages in immutable content from a {@link DataSource}.
+ * <p>
+ * 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)}. To display a PagedList, see {@link PagedListAdapter}, which enables the
+ * binding of a PagedList to a {@link androidx.recyclerview.widget.RecyclerView}.
+ * <h4>Loading Data</h4>
+ * <p>
+ * All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads the
+ * first chunk of 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>
+ * A PagedList initially presents this first partial load as its content, and expands over time as
+ * content is loaded in. 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 LivePagedListBuilder} to get a
+ * {@link androidx.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 androidx.recyclerview.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 does not count its data set in its initial load, or if {@code false} is passed to
+ * {@link Config.Builder#setEnablePlaceholders(boolean)} when building a {@link Config}.
+ * <h4>Mutability and Snapshots</h4>
+ * A PagedList is <em>mutable</em> while loading, or ready to load from its DataSource.
+ * As loads succeed, a mutable PagedList will be updated via Runnables on the main thread. You can
+ * listen to these updates with a {@link Callback}. (Note that {@link PagedListAdapter} will listen
+ * to these to signal RecyclerView about the updates/changes).
+ * <p>
+ * If a PagedList attempts to load from an invalid DataSource, it will {@link #detach()}
+ * from the DataSource, meaning that it will no longer attempt to load data. It will return true
+ * from {@link #isImmutable()}, and a new DataSource / PagedList pair must be created to load
+ * further data. See {@link DataSource} and {@link LivePagedListBuilder} for how new PagedLists are
+ * created to represent changed data.
+ * <p>
+ * A PagedList snapshot is simply an immutable shallow copy of the current state of the PagedList as
+ * a {@code List}. It will reference the same inner items, and contain the same {@code null}
+ * placeholders, if present.
+ *
+ * @param <T> The type of the entries in the list.
+ */
+public abstract class PagedList<T> extends AbstractList<T> {
+ @NonNull
+ final Executor mMainThreadExecutor;
+ @NonNull
+ final Executor mBackgroundThreadExecutor;
+ @Nullable
+ final BoundaryCallback<T> mBoundaryCallback;
+ @NonNull
+ final Config mConfig;
+ @NonNull
+ final PagedStorage<T> mStorage;
+
+ int mLastLoad = 0;
+ T mLastItem = null;
+
+ // if set to true, mBoundaryCallback is non-null, and should
+ // be dispatched when nearby load has occurred
+ private boolean mBoundaryCallbackBeginDeferred = false;
+ private boolean mBoundaryCallbackEndDeferred = false;
+
+ // lowest and highest index accessed by loadAround. Used to
+ // decide when mBoundaryCallback should be dispatched
+ private int mLowestIndexAccessed = Integer.MAX_VALUE;
+ private int mHighestIndexAccessed = Integer.MIN_VALUE;
+
+ private final AtomicBoolean mDetached = new AtomicBoolean(false);
+
+ protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>();
+
+ PagedList(@NonNull PagedStorage<T> storage,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @Nullable BoundaryCallback<T> boundaryCallback,
+ @NonNull Config config) {
+ mStorage = storage;
+ mMainThreadExecutor = mainThreadExecutor;
+ mBackgroundThreadExecutor = backgroundThreadExecutor;
+ mBoundaryCallback = boundaryCallback;
+ mConfig = config;
+ }
+
+ /**
+ * Create a PagedList which loads data from the provided data source on a background thread,
+ * posting updates to the main thread.
+ *
+ *
+ * @param dataSource DataSource providing data to the PagedList
+ * @param mainThreadExecutor Thread that will use and consume data from the PagedList.
+ * Generally, this is the UI/main thread.
+ * @param backgroundThreadExecutor Data loading will be done via this executor - should be a
+ * background thread.
+ * @param boundaryCallback Optional boundary callback to attach to the list.
+ * @param config PagedList Config, which defines how the PagedList will load data.
+ * @param <K> Key type that indicates to the DataSource what data to load.
+ * @param <T> Type of items to be held and loaded by the PagedList.
+ *
+ * @return Newly created PagedList, which will page in data from the DataSource as needed.
+ */
+ @NonNull
+ private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @Nullable BoundaryCallback<T> boundaryCallback,
+ @NonNull Config config,
+ @Nullable K key) {
+ if (dataSource.isContiguous() || !config.enablePlaceholders) {
+ int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
+ if (!dataSource.isContiguous()) {
+ //noinspection unchecked
+ dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
+ .wrapAsContiguousWithoutPlaceholders();
+ if (key != null) {
+ lastLoad = (int) key;
+ }
+ }
+ ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
+ return new ContiguousPagedList<>(contigDataSource,
+ mainThreadExecutor,
+ backgroundThreadExecutor,
+ boundaryCallback,
+ config,
+ key,
+ lastLoad);
+ } else {
+ return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
+ mainThreadExecutor,
+ backgroundThreadExecutor,
+ boundaryCallback,
+ config,
+ (key != null) ? (Integer) key : 0);
+ }
+ }
+
+ /**
+ * Builder class for PagedList.
+ * <p>
+ * DataSource, Config, main thread and background executor must all be provided.
+ * <p>
+ * A PagedList queries initial data from its DataSource during construction, to avoid empty
+ * PagedLists being presented to the UI when possible. It's preferred 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>
+ * {@link LivePagedListBuilder} 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.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static final class Builder<Key, Value> {
+ private final DataSource<Key, Value> mDataSource;
+ private final Config mConfig;
+ private Executor mMainThreadExecutor;
+ private Executor mBackgroundThreadExecutor;
+ private BoundaryCallback mBoundaryCallback;
+ private Key mInitialKey;
+
+ /**
+ * Create a PagedList.Builder with the provided {@link DataSource} and {@link Config}.
+ *
+ * @param dataSource DataSource the PagedList will load from.
+ * @param config Config that defines how the PagedList loads data from its DataSource.
+ */
+ public Builder(@NonNull DataSource<Key, Value> dataSource, @NonNull Config config) {
+ //noinspection ConstantConditions
+ if (dataSource == null) {
+ throw new IllegalArgumentException("DataSource may not be null");
+ }
+ //noinspection ConstantConditions
+ if (config == null) {
+ throw new IllegalArgumentException("Config may not be null");
+ }
+ mDataSource = dataSource;
+ mConfig = config;
+ }
+
+ /**
+ * Create a PagedList.Builder with the provided {@link DataSource} and page size.
+ * <p>
+ * This method is a convenience for:
+ * <pre>
+ * PagedList.Builder(dataSource,
+ * new PagedList.Config.Builder().setPageSize(pageSize).build());
+ * </pre>
+ *
+ * @param dataSource DataSource the PagedList will load from.
+ * @param pageSize Config that defines how the PagedList loads data from its DataSource.
+ */
+ public Builder(@NonNull DataSource<Key, Value> dataSource, int pageSize) {
+ this(dataSource, new PagedList.Config.Builder().setPageSize(pageSize).build());
+ }
+ /**
+ * The executor defining where main/UI thread for page loading updates.
+ *
+ * @param mainThreadExecutor Executor for main/UI thread to receive {@link Callback} calls.
+ * @return this
+ */
+ @NonNull
+ public Builder<Key, Value> setMainThreadExecutor(@NonNull Executor mainThreadExecutor) {
+ mMainThreadExecutor = mainThreadExecutor;
+ return this;
+ }
+
+ /**
+ * The executor on which background loading will be run.
+ * <p>
+ * Does not affect initial load, which will be done on whichever thread the PagedList is
+ * created on.
+ *
+ * @param backgroundThreadExecutor Executor for background DataSource loading.
+ * @return this
+ */
+ @NonNull
+ public Builder<Key, Value> setBackgroundThreadExecutor(
+ @NonNull Executor backgroundThreadExecutor) {
+ mBackgroundThreadExecutor = backgroundThreadExecutor;
+ return this;
+ }
+
+ /**
+ * The BoundaryCallback for out of data events.
+ * <p>
+ * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load.
+ *
+ * @param boundaryCallback BoundaryCallback for listening to out-of-data events.
+ * @return this
+ */
+ @SuppressWarnings("unused")
+ @NonNull
+ public Builder<Key, Value> setBoundaryCallback(
+ @Nullable BoundaryCallback boundaryCallback) {
+ mBoundaryCallback = boundaryCallback;
+ return this;
+ }
+
+ /**
+ * Sets the initial key the DataSource should load around as part of initialization.
+ *
+ * @param initialKey Key the DataSource should load around as part of initialization.
+ * @return this
+ */
+ @NonNull
+ public Builder<Key, Value> setInitialKey(@Nullable Key initialKey) {
+ mInitialKey = initialKey;
+ return this;
+ }
+
+ /**
+ * Creates a {@link PagedList} with the given parameters.
+ * <p>
+ * This call will dispatch the {@link DataSource}'s loadInitial method immediately. If a
+ * DataSource posts all of its work (e.g. to a network thread), the PagedList will
+ * be immediately created as empty, and grow to its initial size when the initial load
+ * completes.
+ * <p>
+ * If the DataSource implements its load synchronously, doing the load work immediately in
+ * the loadInitial method, the PagedList will block on that load before completing
+ * construction. In this case, use a background thread to create a PagedList.
+ * <p>
+ * It's fine to create a PagedList with an async DataSource on the main thread, such as in
+ * the constructor of a ViewModel. An async network load won't block the initialLoad
+ * function. For a synchronous DataSource such as one created from a Room database, a
+ * {@code LiveData<PagedList>} can be safely constructed with {@link LivePagedListBuilder}
+ * on the main thread, since actual construction work is deferred, and done on a background
+ * 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
+ */
+ @WorkerThread
+ @NonNull
+ public PagedList<Value> build() {
+ // TODO: define defaults, once they can be used in module without android dependency
+ if (mMainThreadExecutor == null) {
+ throw new IllegalArgumentException("MainThreadExecutor required");
+ }
+ if (mBackgroundThreadExecutor == null) {
+ throw new IllegalArgumentException("BackgroundThreadExecutor required");
+ }
+
+ //noinspection unchecked
+ return PagedList.create(
+ mDataSource,
+ mMainThreadExecutor,
+ mBackgroundThreadExecutor,
+ mBoundaryCallback,
+ mConfig,
+ mInitialKey);
+ }
+ }
+
+ /**
+ * Get the item in the list of loaded items at the provided index.
+ *
+ * @param index Index in the loaded item list. Must be >= 0, and < {@link #size()}
+ * @return The item at the passed index, or null if a null placeholder is at the specified
+ * position.
+ *
+ * @see #size()
+ */
+ @Override
+ @Nullable
+ public T get(int index) {
+ T item = mStorage.get(index);
+ if (item != null) {
+ mLastItem = item;
+ }
+ return item;
+ }
+
+ /**
+ * Load adjacent items to passed index.
+ *
+ * @param index Index at which to load.
+ */
+ public void loadAround(int index) {
+ mLastLoad = index + getPositionOffset();
+ loadAroundInternal(index);
+
+ mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
+ mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
+
+ /*
+ * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
+ * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
+ * and accesses happen near the boundaries.
+ *
+ * Note: we post here, since RecyclerView may want to add items in response, and this
+ * call occurs in PagedListAdapter bind.
+ */
+ tryDispatchBoundaryCallbacks(true);
+ }
+
+ // Creation thread for initial synchronous load, otherwise main thread
+ // Safe to access main thread only state - no other thread has reference during construction
+ @AnyThread
+ void deferBoundaryCallbacks(final boolean deferEmpty,
+ final boolean deferBegin, final boolean deferEnd) {
+ if (mBoundaryCallback == null) {
+ throw new IllegalStateException("Can't defer BoundaryCallback, no instance");
+ }
+
+ /*
+ * If lowest/highest haven't been initialized, set them to storage size,
+ * since placeholders must already be computed by this point.
+ *
+ * This is just a minor optimization so that BoundaryCallback callbacks are sent immediately
+ * if the initial load size is smaller than the prefetch window (see
+ * TiledPagedListTest#boundaryCallback_immediate())
+ */
+ if (mLowestIndexAccessed == Integer.MAX_VALUE) {
+ mLowestIndexAccessed = mStorage.size();
+ }
+ if (mHighestIndexAccessed == Integer.MIN_VALUE) {
+ mHighestIndexAccessed = 0;
+ }
+
+ if (deferEmpty || deferBegin || deferEnd) {
+ // Post to the main thread, since we may be on creation thread currently
+ mMainThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ // on is dispatched immediately, since items won't be accessed
+ //noinspection ConstantConditions
+ if (deferEmpty) {
+ mBoundaryCallback.onZeroItemsLoaded();
+ }
+
+ // for other callbacks, mark deferred, and only dispatch if loadAround
+ // has been called near to the position
+ if (deferBegin) {
+ mBoundaryCallbackBeginDeferred = true;
+ }
+ if (deferEnd) {
+ mBoundaryCallbackEndDeferred = true;
+ }
+ tryDispatchBoundaryCallbacks(false);
+ }
+ });
+ }
+ }
+
+ /**
+ * Call this when mLowest/HighestIndexAccessed are changed, or
+ * mBoundaryCallbackBegin/EndDeferred is set.
+ */
+ private void tryDispatchBoundaryCallbacks(boolean post) {
+ final boolean dispatchBegin = mBoundaryCallbackBeginDeferred
+ && mLowestIndexAccessed <= mConfig.prefetchDistance;
+ final boolean dispatchEnd = mBoundaryCallbackEndDeferred
+ && mHighestIndexAccessed >= size() - 1 - mConfig.prefetchDistance;
+
+ if (!dispatchBegin && !dispatchEnd) {
+ return;
+ }
+
+ if (dispatchBegin) {
+ mBoundaryCallbackBeginDeferred = false;
+ }
+ if (dispatchEnd) {
+ mBoundaryCallbackEndDeferred = false;
+ }
+ if (post) {
+ mMainThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
+ }
+ });
+ } else {
+ dispatchBoundaryCallbacks(dispatchBegin, dispatchEnd);
+ }
+ }
+
+ private void dispatchBoundaryCallbacks(boolean begin, boolean end) {
+ // safe to deref mBoundaryCallback here, since we only defer if mBoundaryCallback present
+ if (begin) {
+ //noinspection ConstantConditions
+ mBoundaryCallback.onItemAtFrontLoaded(mStorage.getFirstLoadedItem());
+ }
+ if (end) {
+ //noinspection ConstantConditions
+ mBoundaryCallback.onItemAtEndLoaded(mStorage.getLastLoadedItem());
+ }
+ }
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ void offsetBoundaryAccessIndices(int offset) {
+ mLowestIndexAccessed += offset;
+ mHighestIndexAccessed += offset;
+ }
+
+ /**
+ * Returns size of the list, including any not-yet-loaded null padding.
+ *
+ * @return Current total size of the list.
+ */
+ @Override
+ public int size() {
+ return mStorage.size();
+ }
+
+ /**
+ * Returns whether the list is immutable.
+ *
+ * Immutable lists may not become mutable again, and may safely be accessed from any thread.
+ * <p>
+ * In the future, this method may return true when a PagedList has completed loading from its
+ * DataSource. Currently, it is equivalent to {@link #isDetached()}.
+ *
+ * @return True if the PagedList is immutable.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean isImmutable() {
+ return isDetached();
+ }
+
+ /**
+ * Returns an immutable snapshot of the PagedList in its current state.
+ *
+ * If this PagedList {@link #isImmutable() is immutable} due to its DataSource being invalid, it
+ * will be returned.
+ *
+ * @return Immutable snapshot of PagedList data.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @NonNull
+ public List<T> snapshot() {
+ if (isImmutable()) {
+ return this;
+ }
+ return new SnapshotPagedList<>(this);
+ }
+
+ abstract boolean isContiguous();
+
+ /**
+ * Return the Config used to construct this PagedList.
+ *
+ * @return the Config of this PagedList
+ */
+ @NonNull
+ public Config getConfig() {
+ return mConfig;
+ }
+
+ /**
+ * Return the DataSource that provides data to this PagedList.
+ *
+ * @return the DataSource of this PagedList.
+ */
+ @NonNull
+ public abstract DataSource<?, T> getDataSource();
+
+ /**
+ * Return the key for the position passed most recently to {@link #loadAround(int)}.
+ * <p>
+ * When a PagedList is invalidated, you can pass the key returned by this function to initialize
+ * the next PagedList. This ensures (depending on load times) that the next PagedList that
+ * arrives will have data that overlaps. If you use {@link LivePagedListBuilder}, it will do
+ * this for you.
+ *
+ * @return Key of position most recently passed to {@link #loadAround(int)}.
+ */
+ @Nullable
+ public abstract Object getLastKey();
+
+ /**
+ * True if the PagedList has detached the DataSource it was loading from, and will no longer
+ * load new data.
+ * <p>
+ * A detached list is {@link #isImmutable() immutable}.
+ *
+ * @return True if the data source is detached.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean isDetached() {
+ return mDetached.get();
+ }
+
+ /**
+ * Detach the PagedList from its DataSource, and attempt to load no more data.
+ * <p>
+ * This is called automatically when a DataSource load returns <code>null</code>, which is a
+ * signal to stop loading. The PagedList will continue to present existing data, but will not
+ * initiate new loads.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void detach() {
+ mDetached.set(true);
+ }
+
+ /**
+ * Position offset of the data in the list.
+ * <p>
+ * If data is supplied by a {@link PositionalDataSource}, the item returned from
+ * <code>get(i)</code> has a position of <code>i + getPositionOffset()</code>.
+ * <p>
+ * If the DataSource is a {@link ItemKeyedDataSource} or {@link PageKeyedDataSource}, it
+ * doesn't use positions, returns 0.
+ */
+ public int getPositionOffset() {
+ return mStorage.getPositionOffset();
+ }
+
+ /**
+ * Adds a callback, and issues updates since the previousSnapshot was created.
+ * <p>
+ * If previousSnapshot is passed, the callback will also immediately be dispatched any
+ * differences between the previous snapshot, and the current state. For example, if the
+ * previousSnapshot was of 5 nulls, 10 items, 5 nulls, and the current state was 5 nulls,
+ * 12 items, 3 nulls, the callback would immediately receive a call of
+ * <code>onChanged(14, 2)</code>.
+ * <p>
+ * This allows an observer that's currently presenting a snapshot to catch up to the most recent
+ * version, including any changes that may have been made.
+ * <p>
+ * The callback is internally held as weak reference, so PagedList doesn't hold a strong
+ * reference to its observer, such as a {@link PagedListAdapter}. If an adapter were held with a
+ * strong reference, it would be necessary to clear its PagedList observer before it could be
+ * GC'd.
+ *
+ * @param previousSnapshot Snapshot previously captured from this List, or null.
+ * @param callback Callback to dispatch to.
+ *
+ * @see #removeWeakCallback(Callback)
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) {
+ if (previousSnapshot != null && previousSnapshot != this) {
+
+ if (previousSnapshot.isEmpty()) {
+ if (!mStorage.isEmpty()) {
+ // If snapshot is empty, diff is trivial - just notify number new items.
+ // Note: occurs in async init, when snapshot taken before init page arrives
+ callback.onInserted(0, mStorage.size());
+ }
+ } else {
+ PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot;
+
+ //noinspection unchecked
+ dispatchUpdatesSinceSnapshot(storageSnapshot, callback);
+ }
+ }
+
+ // first, clean up any empty weak refs
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback currentCallback = mCallbacks.get(i).get();
+ if (currentCallback == null) {
+ mCallbacks.remove(i);
+ }
+ }
+
+ // then add the new one
+ mCallbacks.add(new WeakReference<>(callback));
+ }
+ /**
+ * Removes a previously added callback.
+ *
+ * @param callback Callback, previously added.
+ * @see #addWeakCallback(List, Callback)
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void removeWeakCallback(@NonNull Callback callback) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback currentCallback = mCallbacks.get(i).get();
+ if (currentCallback == null || currentCallback == callback) {
+ // found callback, or empty weak ref
+ mCallbacks.remove(i);
+ }
+ }
+ }
+
+ void notifyInserted(int position, int count) {
+ if (count != 0) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback callback = mCallbacks.get(i).get();
+ if (callback != null) {
+ callback.onInserted(position, count);
+ }
+ }
+ }
+ }
+
+ void notifyChanged(int position, int count) {
+ if (count != 0) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ Callback callback = mCallbacks.get(i).get();
+
+ if (callback != null) {
+ callback.onChanged(position, count);
+ }
+ }
+ }
+ }
+
+
+
+ /**
+ * Dispatch updates since the non-empty snapshot was taken.
+ *
+ * @param snapshot Non-empty snapshot.
+ * @param callback Callback for updates that have occurred since snapshot.
+ */
+ abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
+ @NonNull Callback callback);
+
+ abstract void loadAroundInternal(int index);
+
+ /**
+ * Callback signaling when content is loaded into the list.
+ * <p>
+ * Can be used to listen to items being paged in and out. These calls will be dispatched on
+ * the executor defined by {@link Builder#setMainThreadExecutor(Executor)}, which defaults to
+ * the main/UI thread.
+ */
+ public abstract static class Callback {
+ /**
+ * Called when null padding items have been loaded to signal newly available data, or when
+ * data that hasn't been used in a while has been dropped, and swapped back to null.
+ *
+ * @param position Position of first newly loaded items, out of total number of items
+ * (including padded nulls).
+ * @param count Number of items loaded.
+ */
+ public abstract void onChanged(int position, int count);
+
+ /**
+ * Called when new items have been loaded at the end or beginning of the list.
+ *
+ * @param position Position of the first newly loaded item (in practice, either
+ * <code>0</code> or <code>size - 1</code>.
+ * @param count Number of items loaded.
+ */
+ public abstract void onInserted(int position, int count);
+
+ /**
+ * Called when items have been removed at the end or beginning of the list, and have not
+ * been replaced by padded nulls.
+ *
+ * @param position Position of the first newly loaded item (in practice, either
+ * <code>0</code> or <code>size - 1</code>.
+ * @param count Number of items loaded.
+ */
+ @SuppressWarnings("unused")
+ public abstract void onRemoved(int position, int count);
+ }
+
+ /**
+ * Configures how a PagedList loads content from its DataSource.
+ * <p>
+ * Use a Config {@link Builder} to construct and define custom loading behavior, such as
+ * {@link Builder#setPageSize(int)}, which defines number of items loaded at a time}.
+ */
+ public static class Config {
+ /**
+ * Size of each page loaded by the PagedList.
+ */
+ public final int pageSize;
+
+ /**
+ * Prefetch distance which defines how far ahead to load.
+ * <p>
+ * If this value is set to 50, the paged list will attempt to load 50 items in advance of
+ * data that's already been accessed.
+ *
+ * @see PagedList#loadAround(int)
+ */
+ @SuppressWarnings("WeakerAccess")
+ public final int prefetchDistance;
+
+ /**
+ * Defines whether the PagedList may display null placeholders, if the DataSource provides
+ * them.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public final boolean enablePlaceholders;
+
+ /**
+ * Size hint for initial load of PagedList, often larger than a regular page.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public final int initialLoadSizeHint;
+
+ private Config(int pageSize, int prefetchDistance,
+ boolean enablePlaceholders, int initialLoadSizeHint) {
+ this.pageSize = pageSize;
+ this.prefetchDistance = prefetchDistance;
+ this.enablePlaceholders = enablePlaceholders;
+ this.initialLoadSizeHint = initialLoadSizeHint;
+ }
+
+ /**
+ * Builder class for {@link Config}.
+ * <p>
+ * You must at minimum specify page size with {@link #setPageSize(int)}.
+ */
+ public static final class Builder {
+ private int mPageSize = -1;
+ private int mPrefetchDistance = -1;
+ private int mInitialLoadSizeHint = -1;
+ private boolean mEnablePlaceholders = true;
+
+ /**
+ * 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
+ */
+ public Builder setPageSize(int pageSize) {
+ this.mPageSize = pageSize;
+ return this;
+ }
+
+ /**
+ * Defines how far from the edge of loaded content an access must be to trigger further
+ * 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
+ */
+ public Builder setPrefetchDistance(int prefetchDistance) {
+ this.mPrefetchDistance = prefetchDistance;
+ return this;
+ }
+
+ /**
+ * Pass false to disable null placeholders in PagedLists using this Config.
+ * <p>
+ * 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).
+ * <p>
+ * 2) placeholders are not disabled on the Config.
+ * <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
+ */
+ @SuppressWarnings("SameParameterValue")
+ public Builder setEnablePlaceholders(boolean enablePlaceholders) {
+ this.mEnablePlaceholders = enablePlaceholders;
+ return this;
+ }
+
+ /**
+ * Defines how many items to load when first load occurs.
+ * <p>
+ * This value is typically larger than page size, so on first load data there's a large
+ * enough range of content loaded to cover small scrolls.
+ * <p>
+ * When using a {@link PositionalDataSource}, the initial load size will be coerced to
+ * an integer multiple of pageSize, to enable efficient tiling.
+ * <p>
+ * If not set, defaults to three times page size.
+ *
+ * @param initialLoadSizeHint Number of items to load while initializing the PagedList.
+ * @return this
+ */
+ @SuppressWarnings("WeakerAccess")
+ public Builder setInitialLoadSizeHint(int initialLoadSizeHint) {
+ this.mInitialLoadSizeHint = initialLoadSizeHint;
+ return this;
+ }
+
+ /**
+ * Creates a {@link Config} with the given parameters.
+ *
+ * @return A new Config.
+ */
+ public Config build() {
+ if (mPageSize < 1) {
+ throw new IllegalArgumentException("Page size must be a positive number");
+ }
+ if (mPrefetchDistance < 0) {
+ mPrefetchDistance = mPageSize;
+ }
+ if (mInitialLoadSizeHint < 0) {
+ mInitialLoadSizeHint = mPageSize * 3;
+ }
+ if (!mEnablePlaceholders && mPrefetchDistance == 0) {
+ throw new IllegalArgumentException("Placeholders and prefetch are the only ways"
+ + " to trigger loading of more data in the PagedList, so either"
+ + " placeholders must be enabled, or prefetch distance must be > 0.");
+ }
+
+ return new Config(mPageSize, mPrefetchDistance,
+ mEnablePlaceholders, mInitialLoadSizeHint);
+ }
+ }
+ }
+
+ /**
+ * Signals when a PagedList has reached the end of available data.
+ * <p>
+ * When local storage is a cache of network data, it's common to set up a streaming pipeline:
+ * Network data is paged into the database, database is paged into UI. Paging from the database
+ * to UI can be done with a {@code LiveData<PagedList>}, but it's still necessary to know when
+ * to trigger network loads.
+ * <p>
+ * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of
+ * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network
+ * load that will write the result directly to the database. Because the database is being
+ * observed, the UI bound to the {@code LiveData<PagedList>} will update automatically to
+ * account for the new items.
+ * <p>
+ * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to
+ * {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple
+ * times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should
+ * avoid triggering it again while the load is ongoing.
+ * <p>
+ * BoundaryCallback only passes the item at front or end of the list. Number of items is not
+ * passed, since it may not be fully computed by the DataSource if placeholders are not
+ * supplied. Keys are not known because the BoundaryCallback is independent of the
+ * DataSource-specific keys, which may be different for local vs remote storage.
+ * <p>
+ * The database + network Repository in the
+ * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
+ * shows how to implement a network BoundaryCallback using
+ * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
+ * handling swipe-to-refresh, network errors, and retry.
+ *
+ * @param <T> Type loaded by the PagedList.
+ */
+ @MainThread
+ public abstract static class BoundaryCallback<T> {
+ /**
+ * Called when zero items are returned from an initial load of the PagedList's data source.
+ */
+ public void onZeroItemsLoaded() {}
+
+ /**
+ * Called when the item at the front of the PagedList has been loaded, and access has
+ * occurred within {@link Config#prefetchDistance} of it.
+ * <p>
+ * No more data will be prepended to the PagedList before this item.
+ *
+ * @param itemAtFront The first item of PagedList
+ */
+ public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}
+
+ /**
+ * Called when the item at the end of the PagedList has been loaded, and access has
+ * occurred within {@link Config#prefetchDistance} of it.
+ * <p>
+ * No more data will be appended to the PagedList after this item.
+ *
+ * @param itemAtEnd The first item of PagedList
+ */
+ public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/PagedStorage.java b/paging/common/src/main/java/androidx/paging/PagedStorage.java
new file mode 100644
index 0000000..66f4ebf
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/PagedStorage.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+
+final class PagedStorage<T> extends AbstractList<T> {
+ /**
+ * Lists instances are compared (with instance equality) to PLACEHOLDER_LIST to check if an item
+ * in that position is already loading. We use a singleton placeholder list that is distinct
+ * from Collections.EMPTY_LIST for safety.
+ */
+ @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
+ private static final List PLACEHOLDER_LIST = new ArrayList();
+
+ // Always set
+ private int mLeadingNullCount;
+ /**
+ * List of pages in storage.
+ *
+ * Two storage modes:
+ *
+ * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled().
+ * Safe to access any item in any page.
+ *
+ * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true.
+ * mPages may have nulls, or placeholder (empty) pages while content is loading.
+ */
+ private final ArrayList<List<T>> mPages;
+ private int mTrailingNullCount;
+
+ private int mPositionOffset;
+ /**
+ * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in
+ * {@link #mPages} may be null, but this value still counts them.
+ */
+ private int mStorageCount;
+
+ // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set
+ private int mPageSize;
+
+ private int mNumberPrepended;
+ private int mNumberAppended;
+
+ PagedStorage() {
+ mLeadingNullCount = 0;
+ mPages = new ArrayList<>();
+ mTrailingNullCount = 0;
+ mPositionOffset = 0;
+ mStorageCount = 0;
+ mPageSize = 1;
+ mNumberPrepended = 0;
+ mNumberAppended = 0;
+ }
+
+ PagedStorage(int leadingNulls, List<T> page, int trailingNulls) {
+ this();
+ init(leadingNulls, page, trailingNulls, 0);
+ }
+
+ private PagedStorage(PagedStorage<T> other) {
+ mLeadingNullCount = other.mLeadingNullCount;
+ mPages = new ArrayList<>(other.mPages);
+ mTrailingNullCount = other.mTrailingNullCount;
+ mPositionOffset = other.mPositionOffset;
+ mStorageCount = other.mStorageCount;
+ mPageSize = other.mPageSize;
+ mNumberPrepended = other.mNumberPrepended;
+ mNumberAppended = other.mNumberAppended;
+ }
+
+ PagedStorage<T> snapshot() {
+ return new PagedStorage<>(this);
+ }
+
+ private void init(int leadingNulls, List<T> page, int trailingNulls, int positionOffset) {
+ mLeadingNullCount = leadingNulls;
+ mPages.clear();
+ mPages.add(page);
+ mTrailingNullCount = trailingNulls;
+
+ mPositionOffset = positionOffset;
+ mStorageCount = page.size();
+
+ // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled
+ // even if it will break if nulls convert.
+ mPageSize = page.size();
+
+ mNumberPrepended = 0;
+ mNumberAppended = 0;
+ }
+
+ void init(int leadingNulls, @NonNull List<T> page, int trailingNulls, int positionOffset,
+ @NonNull Callback callback) {
+ init(leadingNulls, page, trailingNulls, positionOffset);
+ callback.onInitialized(size());
+ }
+
+ @Override
+ public T get(int i) {
+ if (i < 0 || i >= size()) {
+ throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size());
+ }
+
+ // is it definitely outside 'mPages'?
+ int localIndex = i - mLeadingNullCount;
+ if (localIndex < 0 || localIndex >= mStorageCount) {
+ return null;
+ }
+
+ int localPageIndex;
+ int pageInternalIndex;
+
+ if (isTiled()) {
+ // it's inside mPages, and we're tiled. Jump to correct tile.
+ localPageIndex = localIndex / mPageSize;
+ pageInternalIndex = localIndex % mPageSize;
+ } else {
+ // it's inside mPages, but page sizes aren't regular. Walk to correct tile.
+ // Pages can only be null while tiled, so accessing page count is safe.
+ pageInternalIndex = localIndex;
+ final int localPageCount = mPages.size();
+ for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) {
+ int pageSize = mPages.get(localPageIndex).size();
+ if (pageSize > pageInternalIndex) {
+ // stop, found the page
+ break;
+ }
+ pageInternalIndex -= pageSize;
+ }
+ }
+
+ List<T> page = mPages.get(localPageIndex);
+ if (page == null || page.size() == 0) {
+ // can only occur in tiled case, with untouched inner/placeholder pages
+ return null;
+ }
+ return page.get(pageInternalIndex);
+ }
+
+ /**
+ * Returns true if all pages are the same size, except for the last, which may be smaller
+ */
+ boolean isTiled() {
+ return mPageSize > 0;
+ }
+
+ int getLeadingNullCount() {
+ return mLeadingNullCount;
+ }
+
+ int getTrailingNullCount() {
+ return mTrailingNullCount;
+ }
+
+ int getStorageCount() {
+ return mStorageCount;
+ }
+
+ int getNumberAppended() {
+ return mNumberAppended;
+ }
+
+ int getNumberPrepended() {
+ return mNumberPrepended;
+ }
+
+ int getPageCount() {
+ return mPages.size();
+ }
+
+ interface Callback {
+ void onInitialized(int count);
+ void onPagePrepended(int leadingNulls, int changed, int added);
+ void onPageAppended(int endPosition, int changed, int added);
+ void onPagePlaceholderInserted(int pageIndex);
+ void onPageInserted(int start, int count);
+ }
+
+ int getPositionOffset() {
+ return mPositionOffset;
+ }
+
+ @Override
+ public int size() {
+ return mLeadingNullCount + mStorageCount + mTrailingNullCount;
+ }
+
+ int computeLeadingNulls() {
+ int total = mLeadingNullCount;
+ final int pageCount = mPages.size();
+ for (int i = 0; i < pageCount; i++) {
+ List page = mPages.get(i);
+ if (page != null && page != PLACEHOLDER_LIST) {
+ break;
+ }
+ total += mPageSize;
+ }
+ return total;
+ }
+
+ int computeTrailingNulls() {
+ int total = mTrailingNullCount;
+ for (int i = mPages.size() - 1; i >= 0; i--) {
+ List page = mPages.get(i);
+ if (page != null && page != PLACEHOLDER_LIST) {
+ break;
+ }
+ total += mPageSize;
+ }
+ return total;
+ }
+
+ // ---------------- Contiguous API -------------------
+
+ T getFirstLoadedItem() {
+ // safe to access first page's first item here:
+ // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
+ return mPages.get(0).get(0);
+ }
+
+ T getLastLoadedItem() {
+ // safe to access last page's last item here:
+ // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty
+ List<T> page = mPages.get(mPages.size() - 1);
+ return page.get(page.size() - 1);
+ }
+
+ void prependPage(@NonNull List<T> page, @NonNull Callback callback) {
+ final int count = page.size();
+ if (count == 0) {
+ // Nothing returned from source, stop loading in this direction
+ return;
+ }
+ if (mPageSize > 0 && count != mPageSize) {
+ if (mPages.size() == 1 && count > mPageSize) {
+ // prepending to a single item - update current page size to that of 'inner' page
+ mPageSize = count;
+ } else {
+ // no longer tiled
+ mPageSize = -1;
+ }
+ }
+
+ mPages.add(0, page);
+ mStorageCount += count;
+
+ final int changedCount = Math.min(mLeadingNullCount, count);
+ final int addedCount = count - changedCount;
+
+ if (changedCount != 0) {
+ mLeadingNullCount -= changedCount;
+ }
+ mPositionOffset -= addedCount;
+ mNumberPrepended += count;
+
+ callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount);
+ }
+
+ void appendPage(@NonNull List<T> page, @NonNull Callback callback) {
+ final int count = page.size();
+ if (count == 0) {
+ // Nothing returned from source, stop loading in this direction
+ return;
+ }
+
+ if (mPageSize > 0) {
+ // if the previous page was smaller than mPageSize,
+ // or if this page is larger than the previous, disable tiling
+ if (mPages.get(mPages.size() - 1).size() != mPageSize
+ || count > mPageSize) {
+ mPageSize = -1;
+ }
+ }
+
+ mPages.add(page);
+ mStorageCount += count;
+
+ final int changedCount = Math.min(mTrailingNullCount, count);
+ final int addedCount = count - changedCount;
+
+ if (changedCount != 0) {
+ mTrailingNullCount -= changedCount;
+ }
+ mNumberAppended += count;
+ callback.onPageAppended(mLeadingNullCount + mStorageCount - count,
+ changedCount, addedCount);
+ }
+
+ // ------------------ Non-Contiguous API (tiling required) ----------------------
+
+ void initAndSplit(int leadingNulls, @NonNull List<T> multiPageList,
+ int trailingNulls, int positionOffset, int pageSize, @NonNull Callback callback) {
+
+ int pageCount = (multiPageList.size() + (pageSize - 1)) / pageSize;
+ for (int i = 0; i < pageCount; i++) {
+ int beginInclusive = i * pageSize;
+ int endExclusive = Math.min(multiPageList.size(), (i + 1) * pageSize);
+
+ List<T> sublist = multiPageList.subList(beginInclusive, endExclusive);
+
+ if (i == 0) {
+ // Trailing nulls for first page includes other pages in multiPageList
+ int initialTrailingNulls = trailingNulls + multiPageList.size() - sublist.size();
+ init(leadingNulls, sublist, initialTrailingNulls, positionOffset);
+ } else {
+ int insertPosition = leadingNulls + beginInclusive;
+ insertPage(insertPosition, sublist, null);
+ }
+ }
+ callback.onInitialized(size());
+ }
+
+ public void insertPage(int position, @NonNull List<T> page, @Nullable Callback callback) {
+ final int newPageSize = page.size();
+ if (newPageSize != mPageSize) {
+ // differing page size is OK in 2 cases, when the page is being added:
+ // 1) to the end (in which case, ignore new smaller size)
+ // 2) only the last page has been added so far (in which case, adopt new bigger size)
+
+ int size = size();
+ boolean addingLastPage = position == (size - size % mPageSize)
+ && newPageSize < mPageSize;
+ boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1
+ && newPageSize > mPageSize;
+
+ // OK only if existing single page, and it's the last one
+ if (!onlyEndPagePresent && !addingLastPage) {
+ throw new IllegalArgumentException("page introduces incorrect tiling");
+ }
+ if (onlyEndPagePresent) {
+ mPageSize = newPageSize;
+ }
+ }
+
+ int pageIndex = position / mPageSize;
+
+ allocatePageRange(pageIndex, pageIndex);
+
+ int localPageIndex = pageIndex - mLeadingNullCount / mPageSize;
+
+ List<T> oldPage = mPages.get(localPageIndex);
+ if (oldPage != null && oldPage != PLACEHOLDER_LIST) {
+ throw new IllegalArgumentException(
+ "Invalid position " + position + ": data already loaded");
+ }
+ mPages.set(localPageIndex, page);
+ if (callback != null) {
+ callback.onPageInserted(position, page.size());
+ }
+ }
+
+ private void allocatePageRange(final int minimumPage, final int maximumPage) {
+ int leadingNullPages = mLeadingNullCount / mPageSize;
+
+ if (minimumPage < leadingNullPages) {
+ for (int i = 0; i < leadingNullPages - minimumPage; i++) {
+ mPages.add(0, null);
+ }
+ int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize;
+ mStorageCount += newStorageAllocated;
+ mLeadingNullCount -= newStorageAllocated;
+
+ leadingNullPages = minimumPage;
+ }
+ if (maximumPage >= leadingNullPages + mPages.size()) {
+ int newStorageAllocated = Math.min(mTrailingNullCount,
+ (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize);
+ for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) {
+ mPages.add(mPages.size(), null);
+ }
+ mStorageCount += newStorageAllocated;
+ mTrailingNullCount -= newStorageAllocated;
+ }
+ }
+
+ public void allocatePlaceholders(int index, int prefetchDistance,
+ int pageSize, Callback callback) {
+ if (pageSize != mPageSize) {
+ if (pageSize < mPageSize) {
+ throw new IllegalArgumentException("Page size cannot be reduced");
+ }
+ if (mPages.size() != 1 || mTrailingNullCount != 0) {
+ // not in single, last page allocated case - can't change page size
+ throw new IllegalArgumentException(
+ "Page size can change only if last page is only one present");
+ }
+ mPageSize = pageSize;
+ }
+
+ final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
+ int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
+ int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
+
+ allocatePageRange(minimumPage, maximumPage);
+ int leadingNullPages = mLeadingNullCount / mPageSize;
+ for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
+ int localPageIndex = pageIndex - leadingNullPages;
+ if (mPages.get(localPageIndex) == null) {
+ //noinspection unchecked
+ mPages.set(localPageIndex, PLACEHOLDER_LIST);
+ callback.onPagePlaceholderInserted(pageIndex);
+ }
+ }
+ }
+
+ public boolean hasPage(int pageSize, int index) {
+ // NOTE: we pass pageSize here to avoid in case mPageSize
+ // not fully initialized (when last page only one loaded)
+ int leadingNullPages = mLeadingNullCount / pageSize;
+
+ if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) {
+ return false;
+ }
+
+ List<T> page = mPages.get(index - leadingNullPages);
+
+ return page != null && page != PLACEHOLDER_LIST;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount
+ + ", storage " + mStorageCount
+ + ", trailing " + getTrailingNullCount());
+
+ for (int i = 0; i < mPages.size(); i++) {
+ ret.append(" ").append(mPages.get(i));
+ }
+ return ret.toString();
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/PositionalDataSource.java b/paging/common/src/main/java/androidx/paging/PositionalDataSource.java
new file mode 100644
index 0000000..4f7cd3f
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/PositionalDataSource.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.util.Function;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
+ * arbitrary page positions.
+ * <p>
+ * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. If your data source can't support loading arbitrary
+ * requested page sizes (e.g. when network page size constraints are only known at runtime), use
+ * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
+ * <p>
+ * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
+ * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
+ * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
+ * If placeholders are disabled, initialize with the two parameter
+ * {@link LoadInitialCallback#onResult(List, int)}.
+ * <p>
+ * Room can generate a Factory of PositionalDataSources for you:
+ * <pre>
+ * {@literal @}Dao
+ * interface UserDao {
+ * {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
+ * public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
+ * }</pre>
+ *
+ * @param <T> Type of items being loaded by the PositionalDataSource.
+ */
+public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
+
+ /**
+ * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadInitialParams {
+ /**
+ * Initial load position requested.
+ * <p>
+ * Note that this may not be within the bounds of your data set, it may need to be adjusted
+ * before you execute your load.
+ */
+ public final int requestedStartPosition;
+
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Note that this may be larger than available data.
+ */
+ public final int requestedLoadSize;
+
+ /**
+ * Defines page size acceptable for return values.
+ * <p>
+ * List of items passed to the callback must be an integer multiple of page size.
+ */
+ public final int pageSize;
+
+ /**
+ * Defines whether placeholders are enabled, and whether the total count passed to
+ * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
+ */
+ public final boolean placeholdersEnabled;
+
+ public LoadInitialParams(
+ int requestedStartPosition,
+ int requestedLoadSize,
+ int pageSize,
+ boolean placeholdersEnabled) {
+ this.requestedStartPosition = requestedStartPosition;
+ this.requestedLoadSize = requestedLoadSize;
+ this.pageSize = pageSize;
+ this.placeholdersEnabled = placeholdersEnabled;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadRangeParams {
+ /**
+ * Start position of data to load.
+ * <p>
+ * Returned data must start at this position.
+ */
+ public final int startPosition;
+ /**
+ * Number of items to load.
+ * <p>
+ * Returned data must be of this size, unless at end of the list.
+ */
+ public final int loadSize;
+
+ public LoadRangeParams(int startPosition, int loadSize) {
+ this.startPosition = startPosition;
+ this.loadSize = loadSize;
+ }
+ }
+
+ /**
+ * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
+ * to return data, position, and count.
+ * <p>
+ * A callback should be called only once, and may throw if called again.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <T> Type of items being loaded.
+ */
+ public abstract static class LoadInitialCallback<T> {
+ /**
+ * Called to pass initial load state from a DataSource.
+ * <p>
+ * Call this method from your DataSource's {@code loadInitial} function to return data,
+ * and inform how many placeholders should be shown before and after. If counting is cheap
+ * to compute (for example, if a network load returns the information regardless), it's
+ * recommended to pass the total size to the totalCount parameter. If placeholders are not
+ * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
+ * call {@link #onResult(List, int)}.
+ *
+ * @param data List of items loaded from the DataSource. If this is empty, the DataSource
+ * is treated as empty, and no further loads will occur.
+ * @param position Position of the item at the front of the list. If there are {@code N}
+ * items before the items in data that can be loaded from this DataSource,
+ * pass {@code N}.
+ * @param totalCount Total number of items that may be returned from this DataSource.
+ * Includes the number in the initial {@code data} parameter
+ * as well as any items that can be loaded in front or behind of
+ * {@code data}.
+ */
+ public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
+
+ /**
+ * Called to pass initial load state from a DataSource without total count,
+ * when placeholders aren't requested.
+ * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
+ * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
+ * <p>
+ * Call this method from your DataSource's {@code loadInitial} function to return data,
+ * if position is known but total size is not. If placeholders are requested, call the three
+ * parameter variant: {@link #onResult(List, int, int)}.
+ *
+ * @param data List of items loaded from the DataSource. If this is empty, the DataSource
+ * is treated as empty, and no further loads will occur.
+ * @param position Position of the item at the front of the list. If there are {@code N}
+ * items before the items in data that can be provided by this DataSource,
+ * pass {@code N}.
+ */
+ public abstract void onResult(@NonNull List<T> data, int position);
+ }
+
+ /**
+ * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
+ * to return data.
+ * <p>
+ * A callback should be called only once, and may throw if called again.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <T> Type of items being loaded.
+ */
+ public abstract static class LoadRangeCallback<T> {
+ /**
+ * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
+ *
+ * @param data List of items loaded from the DataSource. Must be same size as requested,
+ * unless at end of list.
+ */
+ public abstract void onResult(@NonNull List<T> data);
+ }
+
+ static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
+ final LoadCallbackHelper<T> mCallbackHelper;
+ private final boolean mCountingEnabled;
+ private final int mPageSize;
+
+ LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
+ int pageSize, PageResult.Receiver<T> receiver) {
+ mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
+ mCountingEnabled = countingEnabled;
+ mPageSize = pageSize;
+ if (mPageSize < 1) {
+ throw new IllegalArgumentException("Page size must be non-negative");
+ }
+ }
+
+ @Override
+ public void onResult(@NonNull List<T> data, int position, int totalCount) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
+ if (position + data.size() != totalCount
+ && data.size() % mPageSize != 0) {
+ throw new IllegalArgumentException("PositionalDataSource requires initial load"
+ + " size to be a multiple of page size to support internal tiling."
+ + " loadSize " + data.size() + ", position " + position
+ + ", totalCount " + totalCount + ", pageSize " + mPageSize);
+ }
+
+ if (mCountingEnabled) {
+ int trailingUnloadedCount = totalCount - position - data.size();
+ mCallbackHelper.dispatchResultToReceiver(
+ new PageResult<>(data, position, trailingUnloadedCount, 0));
+ } else {
+ // Only occurs when wrapped as contiguous
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
+ }
+ }
+ }
+
+ @Override
+ public void onResult(@NonNull List<T> data, int position) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ if (position < 0) {
+ throw new IllegalArgumentException("Position must be non-negative");
+ }
+ if (data.isEmpty() && position != 0) {
+ throw new IllegalArgumentException(
+ "Initial result cannot be empty if items are present in data set.");
+ }
+ if (mCountingEnabled) {
+ throw new IllegalStateException("Placeholders requested, but totalCount not"
+ + " provided. Please call the three-parameter onResult method, or"
+ + " disable placeholders in the PagedList.Config");
+ }
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
+ }
+ }
+ }
+
+ static class LoadRangeCallbackImpl<T> extends LoadRangeCallback<T> {
+ private LoadCallbackHelper<T> mCallbackHelper;
+ private final int mPositionOffset;
+ LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
+ @PageResult.ResultType int resultType, int positionOffset,
+ Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
+ mCallbackHelper = new LoadCallbackHelper<>(
+ dataSource, resultType, mainThreadExecutor, receiver);
+ mPositionOffset = positionOffset;
+ }
+
+ @Override
+ public void onResult(@NonNull List<T> data) {
+ if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
+ mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
+ data, 0, 0, mPositionOffset));
+ }
+ }
+ }
+
+ final void dispatchLoadInitial(boolean acceptCount,
+ int requestedStartPosition, int requestedLoadSize, int pageSize,
+ @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
+ LoadInitialCallbackImpl<T> callback =
+ new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
+
+ LoadInitialParams params = new LoadInitialParams(
+ requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
+ loadInitial(params, callback);
+
+ // If initialLoad's callback is not called within the body, we force any following calls
+ // to post to the UI thread. This constructor may be run on a background thread, but
+ // after constructor, mutation must happen on UI thread.
+ callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
+ }
+
+ final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
+ int count, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<T> receiver) {
+ LoadRangeCallback<T> callback = new LoadRangeCallbackImpl<>(
+ this, resultType, startPosition, mainThreadExecutor, receiver);
+ if (count == 0) {
+ callback.onResult(Collections.<T>emptyList());
+ } else {
+ loadRange(new LoadRangeParams(startPosition, count), callback);
+ }
+ }
+
+ /**
+ * Load initial list data.
+ * <p>
+ * This method is called to load the initial page(s) from the DataSource.
+ * <p>
+ * Result list must be a multiple of pageSize to enable efficient tiling.
+ *
+ * @param params Parameters for initial load, including requested start position, load size, and
+ * page size.
+ * @param callback Callback that receives initial load data, including
+ * position and total data set size.
+ */
+ @WorkerThread
+ public abstract void loadInitial(
+ @NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback);
+
+ /**
+ * Called to load a range of data from the DataSource.
+ * <p>
+ * This method is called to load additional pages from the DataSource after the
+ * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
+ * <p>
+ * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
+ * the number of items requested, at the position requested.
+ *
+ * @param params Parameters for load, including start position and load size.
+ * @param callback Callback that receives loaded data.
+ */
+ @WorkerThread
+ public abstract void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback);
+
+ @Override
+ boolean isContiguous() {
+ return false;
+ }
+
+ @NonNull
+ ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
+ return new ContiguousWithoutPlaceholdersWrapper<>(this);
+ }
+
+ /**
+ * Helper for computing an initial position in
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
+ * computed ahead of loading.
+ * <p>
+ * The value computed by this function will do bounds checking, page alignment, and positioning
+ * based on initial load size requested.
+ * <p>
+ * Example usage in a PositionalDataSource subclass:
+ * <pre>
+ * class ItemDataSource extends PositionalDataSource<Item> {
+ * private int computeCount() {
+ * // actual count code here
+ * }
+ *
+ * private List<Item> loadRangeInternal(int startPosition, int loadCount) {
+ * // actual load code here
+ * }
+ *
+ * {@literal @}Override
+ * public void loadInitial({@literal @}NonNull LoadInitialParams params,
+ * {@literal @}NonNull LoadInitialCallback<Item> callback) {
+ * int totalCount = computeCount();
+ * int position = computeInitialLoadPosition(params, totalCount);
+ * int loadSize = computeInitialLoadSize(params, position, totalCount);
+ * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
+ * }
+ *
+ * {@literal @}Override
+ * public void loadRange({@literal @}NonNull LoadRangeParams params,
+ * {@literal @}NonNull LoadRangeCallback<Item> callback) {
+ * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
+ * }
+ * }</pre>
+ *
+ * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
+ * including page size, and requested start/loadSize.
+ * @param totalCount Total size of the data set.
+ * @return Position to start loading at.
+ *
+ * @see #computeInitialLoadSize(LoadInitialParams, int, int)
+ */
+ public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
+ int totalCount) {
+ int position = params.requestedStartPosition;
+ int initialLoadSize = params.requestedLoadSize;
+ int pageSize = params.pageSize;
+
+ int roundedPageStart = Math.round(position / pageSize) * pageSize;
+
+ // maximum start pos is that which will encompass end of list
+ int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
+ roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
+
+ // minimum start position is 0
+ roundedPageStart = Math.max(0, roundedPageStart);
+
+ return roundedPageStart;
+ }
+
+ /**
+ * Helper for computing an initial load size in
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
+ * computed ahead of loading.
+ * <p>
+ * This function takes the requested load size, and bounds checks it against the value returned
+ * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
+ * <p>
+ * Example usage in a PositionalDataSource subclass:
+ * <pre>
+ * class ItemDataSource extends PositionalDataSource<Item> {
+ * private int computeCount() {
+ * // actual count code here
+ * }
+ *
+ * private List<Item> loadRangeInternal(int startPosition, int loadCount) {
+ * // actual load code here
+ * }
+ *
+ * {@literal @}Override
+ * public void loadInitial({@literal @}NonNull LoadInitialParams params,
+ * {@literal @}NonNull LoadInitialCallback<Item> callback) {
+ * int totalCount = computeCount();
+ * int position = computeInitialLoadPosition(params, totalCount);
+ * int loadSize = computeInitialLoadSize(params, position, totalCount);
+ * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
+ * }
+ *
+ * {@literal @}Override
+ * public void loadRange({@literal @}NonNull LoadRangeParams params,
+ * {@literal @}NonNull LoadRangeCallback<Item> callback) {
+ * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
+ * }
+ * }</pre>
+ *
+ * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
+ * including page size, and requested start/loadSize.
+ * @param initialLoadPosition Value returned by
+ * {@link #computeInitialLoadPosition(LoadInitialParams, int)}
+ * @param totalCount Total size of the data set.
+ * @return Number of items to load.
+ *
+ * @see #computeInitialLoadPosition(LoadInitialParams, int)
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
+ int initialLoadPosition, int totalCount) {
+ return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
+ }
+
+ @SuppressWarnings("deprecation")
+ static class ContiguousWithoutPlaceholdersWrapper<Value>
+ extends ContiguousDataSource<Integer, Value> {
+
+ @NonNull
+ final PositionalDataSource<Value> mPositionalDataSource;
+
+ ContiguousWithoutPlaceholdersWrapper(
+ @NonNull PositionalDataSource<Value> positionalDataSource) {
+ mPositionalDataSource = positionalDataSource;
+ }
+
+ @NonNull
+ @Override
+ public <ToValue> DataSource<Integer, ToValue> mapByPage(
+ @NonNull Function<List<Value>, List<ToValue>> function) {
+ throw new UnsupportedOperationException(
+ "Inaccessible inner type doesn't support map op");
+ }
+
+ @NonNull
+ @Override
+ public <ToValue> DataSource<Integer, ToValue> map(
+ @NonNull Function<Value, ToValue> function) {
+ throw new UnsupportedOperationException(
+ "Inaccessible inner type doesn't support map op");
+ }
+
+ @Override
+ void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
+ boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ final int convertPosition = position == null ? 0 : position;
+
+ // Note enablePlaceholders will be false here, but we don't have a way to communicate
+ // this to PositionalDataSource. This is fine, because only the list and its position
+ // offset will be consumed by the LoadInitialCallback.
+ mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
+ pageSize, mainThreadExecutor, receiver);
+ }
+
+ @Override
+ void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ int startIndex = currentEndIndex + 1;
+ mPositionalDataSource.dispatchLoadRange(
+ PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
+ }
+
+ @Override
+ void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+
+ int startIndex = currentBeginIndex - 1;
+ if (startIndex < 0) {
+ // trigger empty list load
+ mPositionalDataSource.dispatchLoadRange(
+ PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
+ } else {
+ int loadSize = Math.min(pageSize, startIndex + 1);
+ startIndex = startIndex - loadSize + 1;
+ mPositionalDataSource.dispatchLoadRange(
+ PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
+ }
+ }
+
+ @Override
+ Integer getKey(int position, Value item) {
+ return position;
+ }
+
+ }
+
+ @NonNull
+ @Override
+ public final <V> PositionalDataSource<V> mapByPage(
+ @NonNull Function<List<T>, List<V>> function) {
+ return new WrapperPositionalDataSource<>(this, function);
+ }
+
+ @NonNull
+ @Override
+ public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
+ return mapByPage(createListFunction(function));
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/SnapshotPagedList.java b/paging/common/src/main/java/androidx/paging/SnapshotPagedList.java
new file mode 100644
index 0000000..aa31b6f
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/SnapshotPagedList.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+class SnapshotPagedList<T> extends PagedList<T> {
+ private final boolean mContiguous;
+ private final Object mLastKey;
+ private final DataSource<?, T> mDataSource;
+
+ SnapshotPagedList(@NonNull PagedList<T> pagedList) {
+ super(pagedList.mStorage.snapshot(),
+ pagedList.mMainThreadExecutor,
+ pagedList.mBackgroundThreadExecutor,
+ null,
+ pagedList.mConfig);
+ mDataSource = pagedList.getDataSource();
+ mContiguous = pagedList.isContiguous();
+ mLastKey = pagedList.getLastKey();
+ }
+
+ @Override
+ public boolean isImmutable() {
+ return true;
+ }
+
+ @Override
+ public boolean isDetached() {
+ return true;
+ }
+
+ @Override
+ boolean isContiguous() {
+ return mContiguous;
+ }
+
+ @Nullable
+ @Override
+ public Object getLastKey() {
+ return mLastKey;
+ }
+
+ @NonNull
+ @Override
+ public DataSource<?, T> getDataSource() {
+ return mDataSource;
+ }
+
+ @Override
+ void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot,
+ @NonNull Callback callback) {
+ }
+
+ @Override
+ void loadAroundInternal(int index) {
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/TiledDataSource.java b/paging/common/src/main/java/androidx/paging/TiledDataSource.java
new file mode 100644
index 0000000..cc9f150
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/TiledDataSource.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+
+import java.util.Collections;
+import java.util.List;
+
+// NOTE: Room 1.0 depends on this class, so it should not be removed until
+// we can require a version of Room that uses PositionalDataSource directly
+/**
+ * @param <T> Type loaded by the TiledDataSource.
+ *
+ * @deprecated Use {@link PositionalDataSource}
+ * @hide
+ */
+@SuppressWarnings("DeprecatedIsStillUsed")
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
+
+ @WorkerThread
+ public abstract int countItems();
+
+ @Override
+ boolean isContiguous() {
+ return false;
+ }
+
+ @WorkerThread
+ public abstract List<T> loadRange(int startPosition, int count);
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
+ int totalCount = countItems();
+ if (totalCount == 0) {
+ callback.onResult(Collections.<T>emptyList(), 0, 0);
+ return;
+ }
+
+ // bound the size requested, based on known count
+ final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
+ final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
+
+ // convert from legacy behavior
+ List<T> list = loadRange(firstLoadPosition, firstLoadSize);
+ if (list != null && list.size() == firstLoadSize) {
+ callback.onResult(list, firstLoadPosition, totalCount);
+ } else {
+ // null list, or size doesn't match request
+ // The size check is a WAR for Room 1.0, subsequent versions do the check in Room
+ invalidate();
+ }
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback) {
+ List<T> list = loadRange(params.startPosition, params.loadSize);
+ if (list != null) {
+ callback.onResult(list);
+ } else {
+ invalidate();
+ }
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/TiledPagedList.java b/paging/common/src/main/java/androidx/paging/TiledPagedList.java
new file mode 100644
index 0000000..0c7e257
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/TiledPagedList.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import java.util.concurrent.Executor;
+
+class TiledPagedList<T> extends PagedList<T>
+ implements PagedStorage.Callback {
+ private final PositionalDataSource<T> mDataSource;
+
+ private PageResult.Receiver<T> mReceiver = new PageResult.Receiver<T>() {
+ // Creation thread for initial synchronous load, otherwise main thread
+ // Safe to access main thread only state - no other thread has reference during construction
+ @AnyThread
+ @Override
+ public void onPageResult(@PageResult.ResultType int type,
+ @NonNull PageResult<T> pageResult) {
+ if (pageResult.isInvalid()) {
+ detach();
+ return;
+ }
+
+ if (isDetached()) {
+ // No op, have detached
+ return;
+ }
+
+ if (type != PageResult.INIT && type != PageResult.TILE) {
+ throw new IllegalArgumentException("unexpected resultType" + type);
+ }
+
+ if (mStorage.getPageCount() == 0) {
+ mStorage.initAndSplit(
+ pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
+ pageResult.positionOffset, mConfig.pageSize, TiledPagedList.this);
+ } else {
+ mStorage.insertPage(pageResult.positionOffset, pageResult.page,
+ TiledPagedList.this);
+ }
+
+ if (mBoundaryCallback != null) {
+ boolean deferEmpty = mStorage.size() == 0;
+ boolean deferBegin = !deferEmpty
+ && pageResult.leadingNulls == 0
+ && pageResult.positionOffset == 0;
+ int size = size();
+ boolean deferEnd = !deferEmpty
+ && ((type == PageResult.INIT && pageResult.trailingNulls == 0)
+ || (type == PageResult.TILE
+ && (pageResult.positionOffset + mConfig.pageSize >= size)));
+ deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
+ }
+ }
+ };
+
+ @WorkerThread
+ TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @Nullable BoundaryCallback<T> boundaryCallback,
+ @NonNull Config config,
+ int position) {
+ super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
+ boundaryCallback, config);
+ mDataSource = dataSource;
+
+ final int pageSize = mConfig.pageSize;
+ mLastLoad = position;
+
+ if (mDataSource.isInvalid()) {
+ detach();
+ } else {
+ final int firstLoadSize =
+ (Math.max(Math.round(mConfig.initialLoadSizeHint / pageSize), 2)) * pageSize;
+
+ final int idealStart = position - firstLoadSize / 2;
+ final int roundedPageStart = Math.max(0, Math.round(idealStart / pageSize) * pageSize);
+
+ mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
+ pageSize, mMainThreadExecutor, mReceiver);
+ }
+ }
+
+ @Override
+ boolean isContiguous() {
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public DataSource<?, T> getDataSource() {
+ return mDataSource;
+ }
+
+ @Nullable
+ @Override
+ public Object getLastKey() {
+ return mLastLoad;
+ }
+
+ @Override
+ protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
+ @NonNull Callback callback) {
+ //noinspection UnnecessaryLocalVariable
+ final PagedStorage<T> snapshot = pagedListSnapshot.mStorage;
+
+ if (snapshot.isEmpty()
+ || mStorage.size() != snapshot.size()) {
+ throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
+ + " to be a snapshot of this PagedList");
+ }
+
+ // loop through each page and signal the callback for any pages that are present now,
+ // but not in the snapshot.
+ final int pageSize = mConfig.pageSize;
+ final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize;
+ final int pageCount = mStorage.getPageCount();
+ for (int i = 0; i < pageCount; i++) {
+ int pageIndex = i + leadingNullPages;
+ int updatedPages = 0;
+ // count number of consecutive pages that were added since the snapshot...
+ while (updatedPages < mStorage.getPageCount()
+ && mStorage.hasPage(pageSize, pageIndex + updatedPages)
+ && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) {
+ updatedPages++;
+ }
+ // and signal them all at once to the callback
+ if (updatedPages > 0) {
+ callback.onChanged(pageIndex * pageSize, pageSize * updatedPages);
+ i += updatedPages - 1;
+ }
+ }
+ }
+
+ @Override
+ protected void loadAroundInternal(int index) {
+ mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this);
+ }
+
+ @Override
+ public void onInitialized(int count) {
+ notifyInserted(0, count);
+ }
+
+ @Override
+ public void onPagePrepended(int leadingNulls, int changed, int added) {
+ throw new IllegalStateException("Contiguous callback on TiledPagedList");
+ }
+
+ @Override
+ public void onPageAppended(int endPosition, int changed, int added) {
+ throw new IllegalStateException("Contiguous callback on TiledPagedList");
+ }
+
+ @Override
+ public void onPagePlaceholderInserted(final int pageIndex) {
+ // placeholder means initialize a load
+ mBackgroundThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (isDetached()) {
+ return;
+ }
+ final int pageSize = mConfig.pageSize;
+
+ if (mDataSource.isInvalid()) {
+ detach();
+ } else {
+ int startPosition = pageIndex * pageSize;
+ int count = Math.min(pageSize, mStorage.size() - startPosition);
+ mDataSource.dispatchLoadRange(
+ PageResult.TILE, startPosition, count, mMainThreadExecutor, mReceiver);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onPageInserted(int start, int count) {
+ notifyChanged(start, count);
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java
new file mode 100644
index 0000000..72a6d5c
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/WrapperItemKeyedDataSource.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+
+class WrapperItemKeyedDataSource<K, A, B> extends ItemKeyedDataSource<K, B> {
+ private final ItemKeyedDataSource<K, A> mSource;
+ private final Function<List<A>, List<B>> mListFunction;
+ private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
+ @Override
+ public void onInvalidated() {
+ invalidate();
+ removeCallback();
+ }
+ };
+
+ private final IdentityHashMap<B, K> mKeyMap = new IdentityHashMap<>();
+
+ WrapperItemKeyedDataSource(ItemKeyedDataSource<K, A> source,
+ Function<List<A>, List<B>> listFunction) {
+ mSource = source;
+ mListFunction = listFunction;
+ mSource.addInvalidatedCallback(mInvalidatedCallback);
+ }
+
+ private void removeCallback() {
+ mSource.removeInvalidatedCallback(mInvalidatedCallback);
+ }
+
+ private List<B> convertWithStashedKeys(List<A> source) {
+ List<B> dest = convert(mListFunction, source);
+ synchronized (mKeyMap) {
+ // synchronize on mKeyMap, since multiple loads may occur simultaneously.
+ // Note: manually sync avoids locking per-item (e.g. Collections.synchronizedMap)
+ for (int i = 0; i < dest.size(); i++) {
+ mKeyMap.put(dest.get(i), mSource.getKey(source.get(i)));
+ }
+ }
+ return dest;
+ }
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams<K> params,
+ final @NonNull LoadInitialCallback<B> callback) {
+ mSource.loadInitial(params, new LoadInitialCallback<A>() {
+ @Override
+ public void onResult(@NonNull List<A> data, int position, int totalCount) {
+ callback.onResult(convertWithStashedKeys(data), position, totalCount);
+ }
+
+ @Override
+ public void onResult(@NonNull List<A> data) {
+ callback.onResult(convertWithStashedKeys(data));
+ }
+ });
+ }
+
+ @Override
+ public void loadAfter(@NonNull LoadParams<K> params,
+ final @NonNull LoadCallback<B> callback) {
+ mSource.loadAfter(params, new LoadCallback<A>() {
+ @Override
+ public void onResult(@NonNull List<A> data) {
+ callback.onResult(convertWithStashedKeys(data));
+ }
+ });
+ }
+
+ @Override
+ public void loadBefore(@NonNull LoadParams<K> params,
+ final @NonNull LoadCallback<B> callback) {
+ mSource.loadBefore(params, new LoadCallback<A>() {
+ @Override
+ public void onResult(@NonNull List<A> data) {
+ callback.onResult(convertWithStashedKeys(data));
+ }
+ });
+ }
+
+ @NonNull
+ @Override
+ public K getKey(@NonNull B item) {
+ synchronized (mKeyMap) {
+ return mKeyMap.get(item);
+ }
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java b/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java
new file mode 100644
index 0000000..7675026
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/WrapperPageKeyedDataSource.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+
+import java.util.List;
+
+class WrapperPageKeyedDataSource<K, A, B> extends PageKeyedDataSource<K, B> {
+ private final PageKeyedDataSource<K, A> mSource;
+ private final Function<List<A>, List<B>> mListFunction;
+ private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
+ @Override
+ public void onInvalidated() {
+ invalidate();
+ removeCallback();
+ }
+ };
+
+ WrapperPageKeyedDataSource(PageKeyedDataSource<K, A> source,
+ Function<List<A>, List<B>> listFunction) {
+ mSource = source;
+ mListFunction = listFunction;
+ mSource.addInvalidatedCallback(mInvalidatedCallback);
+ }
+
+ private void removeCallback() {
+ mSource.removeInvalidatedCallback(mInvalidatedCallback);
+ }
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams<K> params,
+ final @NonNull LoadInitialCallback<K, B> callback) {
+ mSource.loadInitial(params, new LoadInitialCallback<K, A>() {
+ @Override
+ public void onResult(@NonNull List<A> data, int position, int totalCount,
+ @Nullable K previousPageKey, @Nullable K nextPageKey) {
+ callback.onResult(convert(mListFunction, data), position, totalCount,
+ previousPageKey, nextPageKey);
+ }
+
+ @Override
+ public void onResult(@NonNull List<A> data, @Nullable K previousPageKey,
+ @Nullable K nextPageKey) {
+ callback.onResult(convert(mListFunction, data), previousPageKey, nextPageKey);
+ }
+ });
+ }
+
+ @Override
+ public void loadBefore(@NonNull LoadParams<K> params,
+ final @NonNull LoadCallback<K, B> callback) {
+ mSource.loadBefore(params, new LoadCallback<K, A>() {
+ @Override
+ public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
+ callback.onResult(convert(mListFunction, data), adjacentPageKey);
+ }
+ });
+ }
+
+ @Override
+ public void loadAfter(@NonNull LoadParams<K> params,
+ final @NonNull LoadCallback<K, B> callback) {
+ mSource.loadAfter(params, new LoadCallback<K, A>() {
+ @Override
+ public void onResult(@NonNull List<A> data, @Nullable K adjacentPageKey) {
+ callback.onResult(convert(mListFunction, data), adjacentPageKey);
+ }
+ });
+ }
+}
diff --git a/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java b/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java
new file mode 100644
index 0000000..257f6c7
--- /dev/null
+++ b/paging/common/src/main/java/androidx/paging/WrapperPositionalDataSource.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+
+import java.util.List;
+
+class WrapperPositionalDataSource<A, B> extends PositionalDataSource<B> {
+ private final PositionalDataSource<A> mSource;
+ private final Function<List<A>, List<B>> mListFunction;
+
+ private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
+ @Override
+ public void onInvalidated() {
+ invalidate();
+ removeCallback();
+ }
+ };
+
+ WrapperPositionalDataSource(PositionalDataSource<A> source,
+ Function<List<A>, List<B>> listFunction) {
+ mSource = source;
+ mListFunction = listFunction;
+ mSource.addInvalidatedCallback(mInvalidatedCallback);
+ }
+
+ private void removeCallback() {
+ mSource.removeInvalidatedCallback(mInvalidatedCallback);
+ }
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ final @NonNull LoadInitialCallback<B> callback) {
+ mSource.loadInitial(params, new LoadInitialCallback<A>() {
+ @Override
+ public void onResult(@NonNull List<A> data, int position, int totalCount) {
+ callback.onResult(convert(mListFunction, data), position, totalCount);
+ }
+
+ @Override
+ public void onResult(@NonNull List<A> data, int position) {
+ callback.onResult(convert(mListFunction, data), position);
+ }
+ });
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ final @NonNull LoadRangeCallback<B> callback) {
+ mSource.loadRange(params, new LoadRangeCallback<A>() {
+ @Override
+ public void onResult(@NonNull List<A> data) {
+ callback.onResult(convert(mListFunction, data));
+ }
+ });
+ }
+}
diff --git a/paging/common/src/test/java/android/arch/paging/AsyncListDataSource.kt b/paging/common/src/test/java/android/arch/paging/AsyncListDataSource.kt
deleted file mode 100644
index e1c113d..0000000
--- a/paging/common/src/test/java/android/arch/paging/AsyncListDataSource.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 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.paging
-
-class AsyncListDataSource<T>(list: List<T>)
- : PositionalDataSource<T>() {
- private val workItems: MutableList<() -> Unit> = ArrayList()
- private val listDataSource = ListDataSource(list)
-
- override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
- workItems.add {
- listDataSource.loadInitial(params, callback)
- }
- }
-
- override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
- workItems.add {
- listDataSource.loadRange(params, callback)
- }
- }
-
- fun flush() {
- workItems.map { it() }
- workItems.clear()
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt b/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
deleted file mode 100644
index a0dbfb7..0000000
--- a/paging/common/src/test/java/android/arch/paging/ContiguousPagedListTest.kt
+++ /dev/null
@@ -1,519 +0,0 @@
-/*
- * 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.paging
-
-import android.arch.core.util.Function
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
-import java.util.concurrent.Executor
-
-@RunWith(Parameterized::class)
-class ContiguousPagedListTest(private val mCounted: Boolean) {
- private val mMainThread = TestExecutor()
- private val mBackgroundThread = TestExecutor()
-
- private class Item(position: Int) {
- val name: String = "Item $position"
-
- override fun toString(): String {
- return name
- }
- }
-
- private inner class TestSource(val listData: List<Item> = ITEMS)
- : ContiguousDataSource<Int, Item>() {
- override fun dispatchLoadInitial(
- key: Int?,
- initialLoadSize: Int,
- pageSize: Int,
- enablePlaceholders: Boolean,
- mainThreadExecutor: Executor,
- receiver: PageResult.Receiver<Item>) {
-
- val convertPosition = key ?: 0
- val position = Math.max(0, (convertPosition - initialLoadSize / 2))
- val data = getClampedRange(position, position + initialLoadSize)
- val trailingUnloadedCount = listData.size - position - data.size
-
- if (enablePlaceholders && mCounted) {
- receiver.onPageResult(PageResult.INIT,
- PageResult(data, position, trailingUnloadedCount, 0))
- } else {
- // still must pass offset, even if not counted
- receiver.onPageResult(PageResult.INIT,
- PageResult(data, position))
- }
- }
-
- override fun dispatchLoadAfter(
- currentEndIndex: Int,
- currentEndItem: Item,
- pageSize: Int,
- mainThreadExecutor: Executor,
- receiver: PageResult.Receiver<Item>) {
- val startIndex = currentEndIndex + 1
- val data = getClampedRange(startIndex, startIndex + pageSize)
-
- mainThreadExecutor.execute {
- receiver.onPageResult(PageResult.APPEND, PageResult(data, 0, 0, 0))
- }
- }
-
- override fun dispatchLoadBefore(
- currentBeginIndex: Int,
- currentBeginItem: Item,
- pageSize: Int,
- mainThreadExecutor: Executor,
- receiver: PageResult.Receiver<Item>) {
-
- val startIndex = currentBeginIndex - 1
- val data = getClampedRange(startIndex - pageSize + 1, startIndex + 1)
-
- mainThreadExecutor.execute {
- receiver.onPageResult(PageResult.PREPEND, PageResult(data, 0, 0, 0))
- }
- }
-
- override fun getKey(position: Int, item: Item?): Int {
- return 0
- }
-
- private fun getClampedRange(startInc: Int, endExc: Int): List<Item> {
- return listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc))
- }
-
- override fun <ToValue : Any?> mapByPage(function: Function<List<Item>, List<ToValue>>):
- DataSource<Int, ToValue> {
- throw UnsupportedOperationException()
- }
-
- override fun <ToValue : Any?> map(function: Function<Item, ToValue>):
- DataSource<Int, ToValue> {
- throw UnsupportedOperationException()
- }
- }
-
- private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) {
- if (mCounted) {
- // assert nulls + content
- val expected = arrayOfNulls<Item>(ITEMS.size)
- System.arraycopy(ITEMS.toTypedArray(), start, expected, start, count)
- assertArrayEquals(expected, actual.toTypedArray())
-
- val expectedTrailing = ITEMS.size - start - count
- assertEquals(ITEMS.size, actual.size)
- assertEquals((ITEMS.size - start - expectedTrailing),
- actual.storageCount)
- assertEquals(start, actual.leadingNullCount)
- assertEquals(expectedTrailing, actual.trailingNullCount)
- } else {
- assertEquals(ITEMS.subList(start, start + count), actual)
-
- assertEquals(count, actual.size)
- assertEquals(actual.size, actual.storageCount)
- assertEquals(0, actual.leadingNullCount)
- assertEquals(0, actual.trailingNullCount)
- }
- }
-
- private fun verifyRange(start: Int, count: Int, actual: PagedList<Item>) {
- verifyRange(start, count, actual.mStorage)
- }
-
- private fun createCountedPagedList(
- initialPosition: Int,
- pageSize: Int = 20,
- initLoadSize: Int = 40,
- prefetchDistance: Int = 20,
- listData: List<Item> = ITEMS,
- boundaryCallback: PagedList.BoundaryCallback<Item>? = null,
- lastLoad: Int = ContiguousPagedList.LAST_LOAD_UNSPECIFIED
- ): ContiguousPagedList<Int, Item> {
- return ContiguousPagedList(
- TestSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
- PagedList.Config.Builder()
- .setInitialLoadSizeHint(initLoadSize)
- .setPageSize(pageSize)
- .setPrefetchDistance(prefetchDistance)
- .build(),
- initialPosition,
- lastLoad)
- }
-
- @Test
- fun construct() {
- val pagedList = createCountedPagedList(0)
- verifyRange(0, 40, pagedList)
- }
-
- @Test
- fun getDataSource() {
- val pagedList = createCountedPagedList(0)
- assertTrue(pagedList.dataSource is TestSource)
-
- // snapshot keeps same DataSource
- assertSame(pagedList.dataSource,
- (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource)
- }
-
- private fun verifyCallback(callback: PagedList.Callback, countedPosition: Int,
- uncountedPosition: Int) {
- if (mCounted) {
- verify(callback).onChanged(countedPosition, 20)
- } else {
- verify(callback).onInserted(uncountedPosition, 20)
- }
- }
-
- @Test
- fun append() {
- val pagedList = createCountedPagedList(0)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyRange(0, 40, pagedList)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(35)
- drain()
-
- verifyRange(0, 60, pagedList)
- verifyCallback(callback, 40, 40)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prepend() {
- val pagedList = createCountedPagedList(80)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyRange(60, 40, pagedList)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(if (mCounted) 65 else 5)
- drain()
-
- verifyRange(40, 60, pagedList)
- verifyCallback(callback, 40, 0)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun outwards() {
- val pagedList = createCountedPagedList(50)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyRange(30, 40, pagedList)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(if (mCounted) 65 else 35)
- drain()
-
- verifyRange(30, 60, pagedList)
- verifyCallback(callback, 70, 40)
- verifyNoMoreInteractions(callback)
-
- pagedList.loadAround(if (mCounted) 35 else 5)
- drain()
-
- verifyRange(10, 80, pagedList)
- verifyCallback(callback, 10, 0)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun multiAppend() {
- val pagedList = createCountedPagedList(0)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyRange(0, 40, pagedList)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(55)
- drain()
-
- verifyRange(0, 80, pagedList)
- verifyCallback(callback, 40, 40)
- verifyCallback(callback, 60, 60)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun distantPrefetch() {
- val pagedList = createCountedPagedList(0,
- initLoadSize = 10, pageSize = 10, prefetchDistance = 30)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyRange(0, 10, pagedList)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(5)
- drain()
-
- verifyRange(0, 40, pagedList)
-
- pagedList.loadAround(6)
- drain()
-
- // although our prefetch window moves forward, no new load triggered
- verifyRange(0, 40, pagedList)
- }
-
- @Test
- fun appendCallbackAddedLate() {
- val pagedList = createCountedPagedList(0)
- verifyRange(0, 40, pagedList)
-
- pagedList.loadAround(35)
- drain()
- verifyRange(0, 60, pagedList)
-
- // snapshot at 60 items
- val snapshot = pagedList.snapshot() as PagedList<Item>
- verifyRange(0, 60, snapshot)
-
- // load more items...
- pagedList.loadAround(55)
- drain()
- verifyRange(0, 80, pagedList)
- verifyRange(0, 60, snapshot)
-
- // and verify the snapshot hasn't received them
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(snapshot, callback)
- verifyCallback(callback, 60, 60)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prependCallbackAddedLate() {
- val pagedList = createCountedPagedList(80)
- verifyRange(60, 40, pagedList)
-
- pagedList.loadAround(if (mCounted) 65 else 5)
- drain()
- verifyRange(40, 60, pagedList)
-
- // snapshot at 60 items
- val snapshot = pagedList.snapshot() as PagedList<Item>
- verifyRange(40, 60, snapshot)
-
- pagedList.loadAround(if (mCounted) 45 else 5)
- drain()
- verifyRange(20, 80, pagedList)
- verifyRange(40, 60, snapshot)
-
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(snapshot, callback)
- verifyCallback(callback, 40, 0)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun initialLoad_lastLoad() {
- val pagedList = createCountedPagedList(
- initialPosition = 0,
- initLoadSize = 20,
- lastLoad = 4)
- // last load is param passed
- assertEquals(4, pagedList.mLastLoad)
- verifyRange(0, 20, pagedList)
- }
-
- @Test
- fun initialLoad_lastLoadComputed() {
- val pagedList = createCountedPagedList(
- initialPosition = 0,
- initLoadSize = 20,
- lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
- // last load is middle of initial load
- assertEquals(10, pagedList.mLastLoad)
- verifyRange(0, 20, pagedList)
- }
-
- @Test
- fun initialLoadAsync() {
- // Note: ignores Parameterized param
- val asyncDataSource = AsyncListDataSource(ITEMS)
- val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders()
- val pagedList = ContiguousPagedList(
- dataSource, mMainThread, mBackgroundThread, null,
- PagedList.Config.Builder().setPageSize(10).build(), null,
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
-
- assertTrue(pagedList.isEmpty())
- drain()
- assertTrue(pagedList.isEmpty())
- asyncDataSource.flush()
- assertTrue(pagedList.isEmpty())
- mBackgroundThread.executeAll()
- assertTrue(pagedList.isEmpty())
- verifyZeroInteractions(callback)
-
- // Data source defers callbacks until flush, which posts result to main thread
- mMainThread.executeAll()
- assertFalse(pagedList.isEmpty())
- // callback onInsert called once with initial size
- verify(callback).onInserted(0, pagedList.size)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun addWeakCallbackEmpty() {
- // Note: ignores Parameterized param
- val asyncDataSource = AsyncListDataSource(ITEMS)
- val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders()
- val pagedList = ContiguousPagedList(
- dataSource, mMainThread, mBackgroundThread, null,
- PagedList.Config.Builder().setPageSize(10).build(), null,
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
- val callback = mock(PagedList.Callback::class.java)
-
- // capture empty snapshot
- val emptySnapshot = pagedList.snapshot()
- assertTrue(pagedList.isEmpty())
- assertTrue(emptySnapshot.isEmpty())
-
- // verify that adding callback notifies nothing going from empty -> empty
- pagedList.addWeakCallback(emptySnapshot, callback)
- verifyZeroInteractions(callback)
- pagedList.removeWeakCallback(callback)
-
- // data added in asynchronously
- asyncDataSource.flush()
- drain()
- assertFalse(pagedList.isEmpty())
-
- // verify that adding callback notifies insert going from empty -> content
- pagedList.addWeakCallback(emptySnapshot, callback)
- verify(callback).onInserted(0, pagedList.size)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun boundaryCallback_empty() {
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createCountedPagedList(0,
- listData = ArrayList(), boundaryCallback = boundaryCallback)
- assertEquals(0, pagedList.size)
-
- // nothing yet
- verifyNoMoreInteractions(boundaryCallback)
-
- // onZeroItemsLoaded posted, since creation often happens on BG thread
- drain()
- verify(boundaryCallback).onZeroItemsLoaded()
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- @Test
- fun boundaryCallback_singleInitialLoad() {
- val shortList = ITEMS.subList(0, 4)
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createCountedPagedList(0, listData = shortList,
- initLoadSize = shortList.size, boundaryCallback = boundaryCallback)
- assertEquals(shortList.size, pagedList.size)
-
- // nothing yet
- verifyNoMoreInteractions(boundaryCallback)
-
- // onItemAtFrontLoaded / onItemAtEndLoaded posted, since creation often happens on BG thread
- drain()
- pagedList.loadAround(0)
- drain()
- verify(boundaryCallback).onItemAtFrontLoaded(shortList.first())
- verify(boundaryCallback).onItemAtEndLoaded(shortList.last())
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- @Test
- fun boundaryCallback_delayed() {
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createCountedPagedList(90,
- initLoadSize = 20, prefetchDistance = 5, boundaryCallback = boundaryCallback)
- verifyRange(80, 20, pagedList)
-
- // nothing yet
- verifyZeroInteractions(boundaryCallback)
- drain()
- verifyZeroInteractions(boundaryCallback)
-
- // loading around last item causes onItemAtEndLoaded
- pagedList.loadAround(if (mCounted) 99 else 19)
- drain()
- verifyRange(80, 20, pagedList)
- verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
- verifyNoMoreInteractions(boundaryCallback)
-
- // prepending doesn't trigger callback...
- pagedList.loadAround(if (mCounted) 80 else 0)
- drain()
- verifyRange(60, 40, pagedList)
- verifyZeroInteractions(boundaryCallback)
-
- // ...load rest of data, still no dispatch...
- pagedList.loadAround(if (mCounted) 60 else 0)
- drain()
- pagedList.loadAround(if (mCounted) 40 else 0)
- drain()
- pagedList.loadAround(if (mCounted) 20 else 0)
- drain()
- verifyRange(0, 100, pagedList)
- verifyZeroInteractions(boundaryCallback)
-
- // ... finally try prepend, see 0 items, which will dispatch front callback
- pagedList.loadAround(0)
- drain()
- verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- private fun drain() {
- var executed: Boolean
- do {
- executed = mBackgroundThread.executeAll()
- executed = mMainThread.executeAll() || executed
- } while (executed)
- }
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "counted:{0}")
- fun parameters(): Array<Array<Boolean>> {
- return arrayOf(arrayOf(true), arrayOf(false))
- }
-
- private val ITEMS = List(100) { Item(it) }
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/Executors.kt b/paging/common/src/test/java/android/arch/paging/Executors.kt
deleted file mode 100644
index e1c45a7..0000000
--- a/paging/common/src/test/java/android/arch/paging/Executors.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.paging
-
-import org.junit.Assert.fail
-import java.util.LinkedList
-import java.util.concurrent.Executor
-
-class TestExecutor : Executor {
- private val mTasks = LinkedList<Runnable>()
-
- override fun execute(runnable: Runnable) {
- mTasks.add(runnable)
- }
-
- internal fun executeAll(): Boolean {
- val consumed = !mTasks.isEmpty()
-
- var task = mTasks.poll()
- while (task != null) {
- task.run()
- task = mTasks.poll()
- }
- return consumed
- }
-}
-
-class FailExecutor(val string: String = "Executor expected to be unused") : Executor {
- override fun execute(runnable: Runnable?) {
- fail(string)
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
deleted file mode 100644
index 060094a..0000000
--- a/paging/common/src/test/java/android/arch/paging/ItemKeyedDataSourceTest.kt
+++ /dev/null
@@ -1,457 +0,0 @@
-/*
- * 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.paging
-
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-@RunWith(JUnit4::class)
-class ItemKeyedDataSourceTest {
-
- // ----- STANDARD -----
-
- private fun loadInitial(dataSource: ItemDataSource, key: Key?, initialLoadSize: Int,
- enablePlaceholders: Boolean): PageResult<Item> {
- @Suppress("UNCHECKED_CAST")
- val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Item>
- @Suppress("UNCHECKED_CAST")
- val captor = ArgumentCaptor.forClass(PageResult::class.java)
- as ArgumentCaptor<PageResult<Item>>
-
- dataSource.dispatchLoadInitial(key, initialLoadSize,
- /* ignored pageSize */ 10, enablePlaceholders, FailExecutor(), receiver)
-
- verify(receiver).onPageResult(anyInt(), captor.capture())
- verifyNoMoreInteractions(receiver)
- assertNotNull(captor.value)
- return captor.value
- }
-
- @Test
- fun loadInitial() {
- val dataSource = ItemDataSource()
- val result = loadInitial(dataSource, dataSource.getKey(ITEMS_BY_NAME_ID[49]), 10, true)
-
- assertEquals(45, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
- assertEquals(45, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_keyMatchesSingleItem() {
- val dataSource = ItemDataSource(items = ITEMS_BY_NAME_ID.subList(0, 1))
-
- // this is tricky, since load after and load before with the passed key will fail
- val result = loadInitial(dataSource, dataSource.getKey(ITEMS_BY_NAME_ID[0]), 20, true)
-
- assertEquals(0, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(0, 1), result.page)
- assertEquals(0, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_keyMatchesLastItem() {
- val dataSource = ItemDataSource()
-
- // tricky, because load after key is empty, so another load before and load after required
- val key = dataSource.getKey(ITEMS_BY_NAME_ID.last())
- val result = loadInitial(dataSource, key, 20, true)
-
- assertEquals(90, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(90, 100), result.page)
- assertEquals(0, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_nullKey() {
- val dataSource = ItemDataSource()
-
- // dispatchLoadInitial(null, count) == dispatchLoadInitial(count)
- val result = loadInitial(dataSource, null, 10, true)
-
- assertEquals(0, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
- assertEquals(90, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_keyPastEndOfList() {
- val dataSource = ItemDataSource()
-
- // if key is past entire data set, should return last items in data set
- val key = Key("fz", 0)
- val result = loadInitial(dataSource, key, 10, true)
-
- // NOTE: ideally we'd load 10 items here, but it adds complexity and unpredictability to
- // do: load after was empty, so pass full size to load before, since this can incur larger
- // loads than requested (see keyMatchesLastItem test)
- assertEquals(95, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(95, 100), result.page)
- assertEquals(0, result.trailingNulls)
- }
-
- // ----- UNCOUNTED -----
-
- @Test
- fun loadInitial_disablePlaceholders() {
- val dataSource = ItemDataSource()
-
- // dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
- val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
- val result = loadInitial(dataSource, key, 10, false)
-
- assertEquals(0, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
- assertEquals(0, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_uncounted() {
- val dataSource = ItemDataSource(counted = false)
-
- // dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
- val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
- val result = loadInitial(dataSource, key, 10, true)
-
- assertEquals(0, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
- assertEquals(0, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_nullKey_uncounted() {
- val dataSource = ItemDataSource(counted = false)
-
- // dispatchLoadInitial(null, count) == dispatchLoadInitial(count)
- val result = loadInitial(dataSource, null, 10, true)
-
- assertEquals(0, result.leadingNulls)
- assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
- assertEquals(0, result.trailingNulls)
- }
-
- // ----- EMPTY -----
-
- @Test
- fun loadInitial_empty() {
- val dataSource = ItemDataSource(items = ArrayList())
-
- // dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
- val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
- val result = loadInitial(dataSource, key, 10, true)
-
- assertEquals(0, result.leadingNulls)
- assertTrue(result.page.isEmpty())
- assertEquals(0, result.trailingNulls)
- }
-
- @Test
- fun loadInitial_nullKey_empty() {
- val dataSource = ItemDataSource(items = ArrayList())
- val result = loadInitial(dataSource, null, 10, true)
-
- assertEquals(0, result.leadingNulls)
- assertTrue(result.page.isEmpty())
- assertEquals(0, result.trailingNulls)
- }
-
- // ----- Other behavior -----
-
- @Test
- fun loadBefore() {
- val dataSource = ItemDataSource()
- @Suppress("UNCHECKED_CAST")
- val callback = mock(ItemKeyedDataSource.LoadCallback::class.java)
- as ItemKeyedDataSource.LoadCallback<Item>
-
- dataSource.loadBefore(
- ItemKeyedDataSource.LoadParams(dataSource.getKey(ITEMS_BY_NAME_ID[5]), 5), callback)
-
- @Suppress("UNCHECKED_CAST")
- val argument = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<Item>>
- verify(callback).onResult(argument.capture())
- verifyNoMoreInteractions(callback)
-
- val observed = argument.value
-
- assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed)
- }
-
- internal data class Key(val name: String, val id: Int)
-
- internal data class Item(
- val name: String, val id: Int, val balance: Double, val address: String)
-
- internal class ItemDataSource(private val counted: Boolean = true,
- private val items: List<Item> = ITEMS_BY_NAME_ID)
- : ItemKeyedDataSource<Key, Item>() {
-
- override fun loadInitial(
- params: LoadInitialParams<Key>,
- callback: LoadInitialCallback<Item>) {
- val key = params.requestedInitialKey ?: Key("", Integer.MAX_VALUE)
- val start = Math.max(0, findFirstIndexAfter(key) - params.requestedLoadSize / 2)
- val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
-
- if (params.placeholdersEnabled && counted) {
- callback.onResult(items.subList(start, endExclusive), start, items.size)
- } else {
- callback.onResult(items.subList(start, endExclusive))
- }
- }
-
- override fun loadAfter(params: LoadParams<Key>, callback: LoadCallback<Item>) {
- val start = findFirstIndexAfter(params.key)
- val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
-
- callback.onResult(items.subList(start, endExclusive))
- }
-
- override fun loadBefore(params: LoadParams<Key>, callback: LoadCallback<Item>) {
- val firstIndexBefore = findFirstIndexBefore(params.key)
- val endExclusive = Math.max(0, firstIndexBefore + 1)
- val start = Math.max(0, firstIndexBefore - params.requestedLoadSize + 1)
-
- callback.onResult(items.subList(start, endExclusive))
- }
-
- override fun getKey(item: Item): Key {
- return Key(item.name, item.id)
- }
-
- private fun findFirstIndexAfter(key: Key): Int {
- return items.indices.firstOrNull {
- KEY_COMPARATOR.compare(key, getKey(items[it])) < 0
- } ?: items.size
- }
-
- private fun findFirstIndexBefore(key: Key): Int {
- return items.indices.reversed().firstOrNull {
- KEY_COMPARATOR.compare(key, getKey(items[it])) > 0
- } ?: -1
- }
- }
-
- private fun performLoadInitial(
- invalidateDataSource: Boolean = false,
- callbackInvoker: (callback: ItemKeyedDataSource.LoadInitialCallback<String>) -> Unit) {
- val dataSource = object : ItemKeyedDataSource<String, String>() {
- override fun getKey(item: String): String {
- return ""
- }
-
- override fun loadInitial(
- params: LoadInitialParams<String>,
- callback: LoadInitialCallback<String>) {
- if (invalidateDataSource) {
- // invalidate data source so it's invalid when onResult() called
- invalidate()
- }
- callbackInvoker(callback)
- }
-
- override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String>) {
- fail("loadAfter not expected")
- }
-
- override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String>) {
- fail("loadBefore not expected")
- }
- }
-
- ContiguousPagedList<String, String>(
- dataSource, FailExecutor(), FailExecutor(), null,
- PagedList.Config.Builder()
- .setPageSize(10)
- .build(),
- "",
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
- }
-
- @Test
- fun loadInitialCallbackSuccess() = performLoadInitial {
- // LoadInitialCallback correct usage
- it.onResult(listOf("a", "b"), 0, 2)
- }
-
- @Test
- fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
- // Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
- val elevenLetterList = List(11) { "" + 'a' + it }
- it.onResult(elevenLetterList, 0, 12)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackListTooBig() = performLoadInitial {
- // LoadInitialCallback can't accept pos + list > totalCount
- it.onResult(listOf("a", "b", "c"), 0, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackPositionTooLarge() = performLoadInitial {
- // LoadInitialCallback can't accept pos + list > totalCount
- it.onResult(listOf("a", "b"), 1, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackPositionNegative() = performLoadInitial {
- // LoadInitialCallback can't accept negative position
- it.onResult(listOf("a", "b", "c"), -1, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
- // LoadInitialCallback can't accept empty result unless data set is empty
- it.onResult(emptyList(), 0, 2)
- }
-
- @Test
- fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
- // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
- it.onResult(emptyList(), 0, 1)
- }
-
- private abstract class WrapperDataSource<K, A, B>(private val source: ItemKeyedDataSource<K, A>)
- : ItemKeyedDataSource<K, B>() {
- private val invalidatedCallback = DataSource.InvalidatedCallback {
- invalidate()
- removeCallback()
- }
-
- init {
- source.addInvalidatedCallback(invalidatedCallback)
- }
-
- private fun removeCallback() {
- removeInvalidatedCallback(invalidatedCallback)
- }
-
- override fun loadInitial(params: LoadInitialParams<K>, callback: LoadInitialCallback<B>) {
- source.loadInitial(params, object : LoadInitialCallback<A>() {
- override fun onResult(data: List<A>, position: Int, totalCount: Int) {
- callback.onResult(convert(data), position, totalCount)
- }
-
- override fun onResult(data: MutableList<A>) {
- callback.onResult(convert(data))
- }
- })
- }
-
- override fun loadAfter(params: LoadParams<K>, callback: LoadCallback<B>) {
- source.loadAfter(params, object : LoadCallback<A>() {
- override fun onResult(data: MutableList<A>) {
- callback.onResult(convert(data))
- }
- })
- }
-
- override fun loadBefore(params: LoadParams<K>, callback: LoadCallback<B>) {
- source.loadBefore(params, object : LoadCallback<A>() {
- override fun onResult(data: MutableList<A>) {
- callback.onResult(convert(data))
- }
- })
- }
-
- protected abstract fun convert(source: List<A>): List<B>
- }
-
- private data class DecoratedItem(val item: Item)
-
- private class DecoratedWrapperDataSource(private val source: ItemKeyedDataSource<Key, Item>)
- : WrapperDataSource<Key, Item, DecoratedItem>(source) {
- override fun convert(source: List<Item>): List<DecoratedItem> {
- return source.map { DecoratedItem(it) }
- }
-
- override fun getKey(item: DecoratedItem): Key {
- return source.getKey(item.item)
- }
- }
-
- private fun verifyWrappedDataSource(createWrapper:
- (ItemKeyedDataSource<Key, Item>) -> ItemKeyedDataSource<Key, DecoratedItem>) {
- // verify that it's possible to wrap an ItemKeyedDataSource, and add info to its data
-
- val orig = ItemDataSource(items = ITEMS_BY_NAME_ID)
- val wrapper = DecoratedWrapperDataSource(orig)
-
- // load initial
- @Suppress("UNCHECKED_CAST")
- val loadInitialCallback = mock(ItemKeyedDataSource.LoadInitialCallback::class.java)
- as ItemKeyedDataSource.LoadInitialCallback<DecoratedItem>
- val initKey = orig.getKey(ITEMS_BY_NAME_ID.first())
- wrapper.loadInitial(ItemKeyedDataSource.LoadInitialParams(initKey, 10, false),
- loadInitialCallback)
- verify(loadInitialCallback).onResult(
- ITEMS_BY_NAME_ID.subList(0, 10).map { DecoratedItem(it) })
- verifyNoMoreInteractions(loadInitialCallback)
-
- @Suppress("UNCHECKED_CAST")
- val loadCallback = mock(ItemKeyedDataSource.LoadCallback::class.java)
- as ItemKeyedDataSource.LoadCallback<DecoratedItem>
- val key = orig.getKey(ITEMS_BY_NAME_ID[20])
- // load after
- wrapper.loadAfter(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
- verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(21, 31).map { DecoratedItem(it) })
- verifyNoMoreInteractions(loadCallback)
-
- // load before
- wrapper.loadBefore(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
- verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(10, 20).map { DecoratedItem(it) })
- verifyNoMoreInteractions(loadCallback)
- }
-
- @Test
- fun testManualWrappedDataSource() = verifyWrappedDataSource {
- DecoratedWrapperDataSource(it)
- }
-
- @Test
- fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
- it.mapByPage { it.map { DecoratedItem(it) } }
- }
-
- @Test
- fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
- it.map { DecoratedItem(it) }
- }
-
- companion object {
- private val ITEM_COMPARATOR = compareBy<Item>({ it.name }).thenByDescending({ it.id })
- private val KEY_COMPARATOR = compareBy<Key>({ it.name }).thenByDescending({ it.id })
-
- private val ITEMS_BY_NAME_ID = List(100) {
- val names = Array(10) { "f" + ('a' + it) }
- Item(names[it % 10],
- it,
- Math.random() * 1000,
- (Math.random() * 200).toInt().toString() + " fake st.")
- }.sortedWith(ITEM_COMPARATOR)
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
deleted file mode 100644
index f7ea4b6..0000000
--- a/paging/common/src/test/java/android/arch/paging/PageKeyedDataSourceTest.kt
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * Copyright 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.paging
-
-import org.junit.Assert.assertEquals
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-@RunWith(JUnit4::class)
-class PageKeyedDataSourceTest {
- private val mMainThread = TestExecutor()
- private val mBackgroundThread = TestExecutor()
-
- internal data class Item(val name: String)
-
- internal data class Page(val prev: String?, val data: List<Item>, val next: String?)
-
- internal class ItemDataSource(val data: Map<String, Page> = PAGE_MAP)
- : PageKeyedDataSource<String, Item>() {
-
- private fun getPage(key: String): Page = data[key]!!
-
- override fun loadInitial(
- params: LoadInitialParams<String>,
- callback: LoadInitialCallback<String, Item>) {
- val page = getPage(INIT_KEY)
- callback.onResult(page.data, page.prev, page.next)
- }
-
- override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
- val page = getPage(params.key)
- callback.onResult(page.data, page.prev)
- }
-
- override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
- val page = getPage(params.key)
- callback.onResult(page.data, page.next)
- }
- }
-
- @Test
- fun loadFullVerify() {
- // validate paging entire ItemDataSource results in full, correctly ordered data
- val pagedList = ContiguousPagedList<String, Item>(ItemDataSource(),
- mMainThread, mBackgroundThread,
- null, PagedList.Config.Builder().setPageSize(100).build(), null,
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
-
- // validate initial load
- assertEquals(PAGE_MAP[INIT_KEY]!!.data, pagedList)
-
- // flush the remaining loads
- for (i in 0..PAGE_MAP.keys.size) {
- pagedList.loadAround(0)
- pagedList.loadAround(pagedList.size - 1)
- drain()
- }
-
- // validate full load
- assertEquals(ITEM_LIST, pagedList)
- }
-
- private fun performLoadInitial(invalidateDataSource: Boolean = false,
- callbackInvoker:
- (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit) {
- val dataSource = object : PageKeyedDataSource<String, String>() {
- override fun loadInitial(
- params: LoadInitialParams<String>,
- callback: LoadInitialCallback<String, String>) {
- if (invalidateDataSource) {
- // invalidate data source so it's invalid when onResult() called
- invalidate()
- }
- callbackInvoker(callback)
- }
-
- override fun loadBefore(
- params: LoadParams<String>,
- callback: LoadCallback<String, String>) {
- fail("loadBefore not expected")
- }
-
- override fun loadAfter(
- params: LoadParams<String>,
- callback: LoadCallback<String, String>) {
- fail("loadAfter not expected")
- }
- }
-
- ContiguousPagedList<String, String>(
- dataSource, FailExecutor(), FailExecutor(), null,
- PagedList.Config.Builder()
- .setPageSize(10)
- .build(),
- "",
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
- }
-
- @Test
- fun loadInitialCallbackSuccess() = performLoadInitial {
- // LoadInitialCallback correct usage
- it.onResult(listOf("a", "b"), 0, 2, null, null)
- }
-
- @Test
- fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
- // Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
- val elevenLetterList = List(11) { "" + 'a' + it }
- it.onResult(elevenLetterList, 0, 12, null, null)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackListTooBig() = performLoadInitial {
- // LoadInitialCallback can't accept pos + list > totalCount
- it.onResult(listOf("a", "b", "c"), 0, 2, null, null)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackPositionTooLarge() = performLoadInitial {
- // LoadInitialCallback can't accept pos + list > totalCount
- it.onResult(listOf("a", "b"), 1, 2, null, null)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackPositionNegative() = performLoadInitial {
- // LoadInitialCallback can't accept negative position
- it.onResult(listOf("a", "b", "c"), -1, 2, null, null)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun loadInitialCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
- // LoadInitialCallback can't accept empty result unless data set is empty
- it.onResult(emptyList(), 0, 2, null, null)
- }
-
- @Test
- fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
- // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
- it.onResult(emptyList(), 0, 1, null, null)
- }
-
- private abstract class WrapperDataSource<K, A, B>(private val source: PageKeyedDataSource<K, A>)
- : PageKeyedDataSource<K, B>() {
- private val invalidatedCallback = DataSource.InvalidatedCallback {
- invalidate()
- removeCallback()
- }
-
- init {
- source.addInvalidatedCallback(invalidatedCallback)
- }
-
- private fun removeCallback() {
- removeInvalidatedCallback(invalidatedCallback)
- }
-
- override fun loadInitial(params: LoadInitialParams<K>,
- callback: LoadInitialCallback<K, B>) {
- source.loadInitial(params, object : LoadInitialCallback<K, A>() {
- override fun onResult(data: List<A>, position: Int, totalCount: Int,
- previousPageKey: K?, nextPageKey: K?) {
- callback.onResult(convert(data), position, totalCount,
- previousPageKey, nextPageKey)
- }
-
- override fun onResult(data: MutableList<A>, previousPageKey: K?, nextPageKey: K?) {
- callback.onResult(convert(data), previousPageKey, nextPageKey)
- }
- })
- }
-
- override fun loadBefore(params: LoadParams<K>, callback: LoadCallback<K, B>) {
- source.loadBefore(params, object : LoadCallback<K, A>() {
- override fun onResult(data: List<A>, adjacentPageKey: K?) {
- callback.onResult(convert(data), adjacentPageKey)
- }
- })
- }
-
- override fun loadAfter(params: LoadParams<K>, callback: LoadCallback<K, B>) {
- source.loadAfter(params, object : LoadCallback<K, A>() {
- override fun onResult(data: List<A>, adjacentPageKey: K?) {
- callback.onResult(convert(data), adjacentPageKey)
- }
- })
- }
-
- protected abstract fun convert(source: List<A>): List<B>
- }
-
- private class StringWrapperDataSource<K, V>(source: PageKeyedDataSource<K, V>)
- : WrapperDataSource<K, V, String>(source) {
- override fun convert(source: List<V>): List<String> {
- return source.map { it.toString() }
- }
- }
-
- private fun verifyWrappedDataSource(createWrapper:
- (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>) {
- // verify that it's possible to wrap a PageKeyedDataSource, and add info to its data
- val orig = ItemDataSource(data = PAGE_MAP)
- val wrapper = createWrapper(orig)
-
- // load initial
- @Suppress("UNCHECKED_CAST")
- val loadInitialCallback = mock(PageKeyedDataSource.LoadInitialCallback::class.java)
- as PageKeyedDataSource.LoadInitialCallback<String, String>
-
- wrapper.loadInitial(PageKeyedDataSource.LoadInitialParams<String>(4, true),
- loadInitialCallback)
- val expectedInitial = PAGE_MAP.get(INIT_KEY)!!
- verify(loadInitialCallback).onResult(expectedInitial.data.map { it.toString() },
- expectedInitial.prev, expectedInitial.next)
- verifyNoMoreInteractions(loadInitialCallback)
-
- @Suppress("UNCHECKED_CAST")
- val loadCallback = mock(PageKeyedDataSource.LoadCallback::class.java)
- as PageKeyedDataSource.LoadCallback<String, String>
- // load after
- wrapper.loadAfter(PageKeyedDataSource.LoadParams(expectedInitial.next!!, 4), loadCallback)
- val expectedAfter = PAGE_MAP.get(expectedInitial.next)!!
- verify(loadCallback).onResult(expectedAfter.data.map { it.toString() },
- expectedAfter.next)
- verifyNoMoreInteractions(loadCallback)
-
- // load before
- wrapper.loadBefore(PageKeyedDataSource.LoadParams(expectedAfter.prev!!, 4), loadCallback)
- verify(loadCallback).onResult(expectedInitial.data.map { it.toString() },
- expectedInitial.prev)
- verifyNoMoreInteractions(loadCallback)
- }
-
- @Test
- fun testManualWrappedDataSource() = verifyWrappedDataSource {
- StringWrapperDataSource(it)
- }
-
- @Test
- fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
- it.mapByPage { it.map { it.toString() } }
- }
- @Test
- fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
- it.map { it.toString() }
- }
-
- companion object {
- // first load is 2nd page to ensure we test prepend as well as append behavior
- private val INIT_KEY: String = "key 2"
- private val PAGE_MAP: Map<String, Page>
- private val ITEM_LIST: List<Item>
-
- init {
- val map = HashMap<String, Page>()
- val list = ArrayList<Item>()
- val pageCount = 5
- for (i in 1..pageCount) {
- val data = List(4) { Item("name $i $it") }
- list.addAll(data)
-
- val key = "key $i"
- val prev = if (i > 1) ("key " + (i - 1)) else null
- val next = if (i < pageCount) ("key " + (i + 1)) else null
- map.put(key, Page(prev, data, next))
- }
- PAGE_MAP = map
- ITEM_LIST = list
- }
- }
-
- private fun drain() {
- var executed: Boolean
- do {
- executed = mBackgroundThread.executeAll()
- executed = mMainThread.executeAll() || executed
- } while (executed)
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt b/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
deleted file mode 100644
index 9b53f62..0000000
--- a/paging/common/src/test/java/android/arch/paging/PagedStorageTest.kt
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * 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.paging
-
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-@RunWith(JUnit4::class)
-class PagedStorageTest {
- private fun createPage(vararg strings: String): List<String> {
- return strings.asList()
- }
-
- @Test
- fun construct() {
- val storage = PagedStorage(2, createPage("a", "b"), 2)
-
- assertArrayEquals(arrayOf(null, null, "a", "b", null, null), storage.toArray())
- assertEquals(6, storage.size)
- }
-
- @Test
- fun appendFill() {
- val callback = mock(PagedStorage.Callback::class.java)
-
- val storage = PagedStorage(2, createPage("a", "b"), 2)
- storage.appendPage(createPage("c", "d"), callback)
-
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
- verify(callback).onPageAppended(4, 2, 0)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun appendAdd() {
- val callback = mock(PagedStorage.Callback::class.java)
-
- val storage = PagedStorage(2, createPage("a", "b"), 0)
- storage.appendPage(createPage("c", "d"), callback)
-
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
- verify(callback).onPageAppended(4, 0, 2)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun appendFillAdd() {
- val callback = mock(PagedStorage.Callback::class.java)
-
- val storage = PagedStorage(2, createPage("a", "b"), 2)
-
- // change 2 nulls into c, d
- storage.appendPage(createPage("c", "d"), callback)
-
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
- verify(callback).onPageAppended(4, 2, 0)
- verifyNoMoreInteractions(callback)
-
- // append e, f
- storage.appendPage(createPage("e", "f"), callback)
-
- assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d", "e", "f"), storage.toArray())
- verify(callback).onPageAppended(6, 0, 2)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prependFill() {
- val callback = mock(PagedStorage.Callback::class.java)
-
- val storage = PagedStorage(2, createPage("c", "d"), 2)
- storage.prependPage(createPage("a", "b"), callback)
-
- assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
- verify(callback).onPagePrepended(0, 2, 0)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prependAdd() {
- val callback = mock(PagedStorage.Callback::class.java)
-
- val storage = PagedStorage(0, createPage("c", "d"), 2)
- storage.prependPage(createPage("a", "b"), callback)
-
- assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
- verify(callback).onPagePrepended(0, 0, 2)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prependFillAdd() {
- val callback = mock(PagedStorage.Callback::class.java)
-
- val storage = PagedStorage(2, createPage("e", "f"), 2)
-
- // change 2 nulls into c, d
- storage.prependPage(createPage("c", "d"), callback)
-
- assertArrayEquals(arrayOf("c", "d", "e", "f", null, null), storage.toArray())
- verify(callback).onPagePrepended(0, 2, 0)
- verifyNoMoreInteractions(callback)
-
- // prepend a, b
- storage.prependPage(createPage("a", "b"), callback)
-
- assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null, null), storage.toArray())
- verify(callback).onPagePrepended(0, 0, 2)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun isTiled_addend_smallerPageIsNotLast() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage(0, createPage("a", "a"), 0)
- assertTrue(storage.isTiled)
-
- storage.appendPage(createPage("a", "a"), callback)
- assertTrue(storage.isTiled)
-
- storage.appendPage(createPage("a"), callback)
- assertTrue(storage.isTiled)
-
- // no matter what we append here, we're no longer tiled
- storage.appendPage(createPage("a", "a"), callback)
- assertFalse(storage.isTiled)
- }
-
- @Test
- fun isTiled_append_growingSizeDisable() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage(0, createPage("a", "a"), 0)
- assertTrue(storage.isTiled)
-
- // page size can't grow from append
- storage.appendPage(createPage("a", "a", "a"), callback)
- assertFalse(storage.isTiled)
- }
-
- @Test
- fun isTiled_prepend_smallerPage() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage(0, createPage("a"), 0)
- assertTrue(storage.isTiled)
-
- storage.prependPage(createPage("a", "a"), callback)
- assertTrue(storage.isTiled)
-
- storage.prependPage(createPage("a", "a"), callback)
- assertTrue(storage.isTiled)
-
- storage.prependPage(createPage("a"), callback)
- assertFalse(storage.isTiled)
- }
-
- @Test
- fun isTiled_prepend_smallerThanInitialPage() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage(0, createPage("a", "a"), 0)
- assertTrue(storage.isTiled)
-
- storage.prependPage(createPage("a"), callback)
- assertFalse(storage.isTiled)
- }
-
- @Test
- fun get_tiled() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage(1, createPage("a", "b"), 5)
- assertTrue(storage.isTiled)
-
- storage.appendPage(createPage("c", "d"), callback)
- storage.appendPage(createPage("e", "f"), callback)
-
- assertTrue(storage.isTiled)
- assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
- }
-
- @Test
- fun get_nonTiled() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage(1, createPage("a"), 6)
- assertTrue(storage.isTiled)
-
- storage.appendPage(createPage("b", "c"), callback)
- storage.appendPage(createPage("d", "e", "f"), callback)
-
- assertFalse(storage.isTiled)
- assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
- }
-
- @Test
- fun insertOne() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(2, createPage("c", "d"), 3, 0, callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
- verify(callback).onInitialized(7)
- verifyNoMoreInteractions(callback)
-
- storage.insertPage(4, createPage("e", "f"), callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf(null, null, "c", "d", "e", "f", null), storage.toArray())
- verify(callback).onPageInserted(4, 2)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun insertThree() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(2, createPage("c", "d"), 3, 0, callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
- verify(callback).onInitialized(7)
- verifyNoMoreInteractions(callback)
-
- // first, insert 1st page
- storage.insertPage(0, createPage("a", "b"), callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf("a", "b", "c", "d", null, null, null), storage.toArray())
- verify(callback).onPageInserted(0, 2)
- verifyNoMoreInteractions(callback)
-
- // then 3rd page
- storage.insertPage(4, createPage("e", "f"), callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null), storage.toArray())
- verify(callback).onPageInserted(4, 2)
- verifyNoMoreInteractions(callback)
-
- // then last, small page
- storage.insertPage(6, createPage("g"), callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", "g"), storage.toArray())
- verify(callback).onPageInserted(6, 1)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun insertLastFirst() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(6, createPage("g"), 0, 0, callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf(null, null, null, null, null, null, "g"), storage.toArray())
- verify(callback).onInitialized(7)
- verifyNoMoreInteractions(callback)
-
- // insert 1st page
- storage.insertPage(0, createPage("a", "b"), callback)
-
- assertEquals(7, storage.size)
- assertArrayEquals(arrayOf("a", "b", null, null, null, null, "g"), storage.toArray())
- verify(callback).onPageInserted(0, 2)
- verifyNoMoreInteractions(callback)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun insertFailure_decreaseLast() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(2, createPage("c", "d"), 0, 0, callback)
-
- // should throw, page too small
- storage.insertPage(0, createPage("a"), callback)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun insertFailure_increase() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(0, createPage("a", "b"), 3, 0, callback)
-
- // should throw, page too big
- storage.insertPage(2, createPage("c", "d", "e"), callback)
- }
-
- @Test
- fun allocatePlaceholders_simple() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(2, createPage("c"), 2, 0, callback)
-
- verify(callback).onInitialized(5)
-
- storage.allocatePlaceholders(2, 1, 1, callback)
-
- verify(callback).onPagePlaceholderInserted(1)
- verify(callback).onPagePlaceholderInserted(3)
- verifyNoMoreInteractions(callback)
-
- assertArrayEquals(arrayOf(null, null, "c", null, null), storage.toArray())
- }
-
- @Test
- fun allocatePlaceholders_adoptPageSize() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(4, createPage("e"), 0, 0, callback)
-
- verify(callback).onInitialized(5)
-
- storage.allocatePlaceholders(0, 2, 2, callback)
-
- verify(callback).onPagePlaceholderInserted(0)
- verify(callback).onPagePlaceholderInserted(1)
- verifyNoMoreInteractions(callback)
-
- assertArrayEquals(arrayOf(null, null, null, null, "e"), storage.toArray())
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun allocatePlaceholders_cannotShrinkPageSize() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(4, createPage("e", "f"), 0, 0, callback)
-
- verify(callback).onInitialized(6)
-
- storage.allocatePlaceholders(0, 2, 1, callback)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun allocatePlaceholders_cannotAdoptPageSize() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(2, createPage("c", "d"), 2, 0, callback)
-
- verify(callback).onInitialized(6)
-
- storage.allocatePlaceholders(0, 2, 3, callback)
- }
-
- @Test
- fun get_placeholdersMulti() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(2, createPage("c", "d"), 3, 0, callback)
-
- assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
-
- storage.allocatePlaceholders(0, 10, 2, callback)
-
- // allocating placeholders shouldn't affect result of get
- assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
- }
-
- @Test
- fun hasPage() {
- val callback = mock(PagedStorage.Callback::class.java)
- val storage = PagedStorage<String>()
-
- storage.init(4, createPage("e"), 0, 0, callback)
-
- assertFalse(storage.hasPage(1, 0))
- assertFalse(storage.hasPage(1, 1))
- assertFalse(storage.hasPage(1, 2))
- assertFalse(storage.hasPage(1, 3))
- assertTrue(storage.hasPage(1, 4))
-
- assertFalse(storage.hasPage(2, 0))
- assertFalse(storage.hasPage(2, 1))
- assertTrue(storage.hasPage(2, 2))
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
deleted file mode 100644
index eab778c..0000000
--- a/paging/common/src/test/java/android/arch/paging/PositionalDataSourceTest.kt
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
- * Copyright 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.paging
-
-import org.junit.Assert.assertEquals
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-@RunWith(JUnit4::class)
-class PositionalDataSourceTest {
- private fun computeInitialLoadPos(
- requestedStartPosition: Int,
- requestedLoadSize: Int,
- pageSize: Int,
- totalCount: Int): Int {
- val params = PositionalDataSource.LoadInitialParams(
- requestedStartPosition, requestedLoadSize, pageSize, true)
- return PositionalDataSource.computeInitialLoadPosition(params, totalCount)
- }
-
- @Test
- fun computeInitialLoadPositionZero() {
- assertEquals(0, computeInitialLoadPos(
- requestedStartPosition = 0,
- requestedLoadSize = 30,
- pageSize = 10,
- totalCount = 100))
- }
-
- @Test
- fun computeInitialLoadPositionRequestedPositionIncluded() {
- assertEquals(10, computeInitialLoadPos(
- requestedStartPosition = 10,
- requestedLoadSize = 10,
- pageSize = 10,
- totalCount = 100))
- }
-
- @Test
- fun computeInitialLoadPositionRound() {
- assertEquals(10, computeInitialLoadPos(
- requestedStartPosition = 13,
- requestedLoadSize = 30,
- pageSize = 10,
- totalCount = 100))
- }
-
- @Test
- fun computeInitialLoadPositionEndAdjusted() {
- assertEquals(70, computeInitialLoadPos(
- requestedStartPosition = 99,
- requestedLoadSize = 30,
- pageSize = 10,
- totalCount = 100))
- }
-
- @Test
- fun computeInitialLoadPositionEndAdjustedAndAligned() {
- assertEquals(70, computeInitialLoadPos(
- requestedStartPosition = 99,
- requestedLoadSize = 35,
- pageSize = 10,
- totalCount = 100))
- }
-
- @Test
- fun fullLoadWrappedAsContiguous() {
- // verify that prepend / append work correctly with a PositionalDataSource, made contiguous
- val config = PagedList.Config.Builder()
- .setPageSize(10)
- .setInitialLoadSizeHint(10)
- .setEnablePlaceholders(true)
- .build()
- val dataSource: PositionalDataSource<Int> = ListDataSource((0..99).toList())
- val testExecutor = TestExecutor()
- val pagedList = ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
- testExecutor, testExecutor, null, config, 15,
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
-
- assertEquals((10..19).toList(), pagedList)
-
- // prepend + append work correctly
- pagedList.loadAround(5)
- testExecutor.executeAll()
- assertEquals((0..29).toList(), pagedList)
-
- // and load the rest of the data to be sure further appends work
- for (i in (2..9)) {
- pagedList.loadAround(i * 10 - 5)
- testExecutor.executeAll()
- assertEquals((0..i * 10 + 9).toList(), pagedList)
- }
- }
-
- private fun performLoadInitial(
- enablePlaceholders: Boolean = true,
- invalidateDataSource: Boolean = false,
- callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit) {
- val dataSource = object : PositionalDataSource<String>() {
- override fun loadInitial(
- params: LoadInitialParams,
- callback: LoadInitialCallback<String>) {
- if (invalidateDataSource) {
- // invalidate data source so it's invalid when onResult() called
- invalidate()
- }
- callbackInvoker(callback)
- }
-
- override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<String>) {
- fail("loadRange not expected")
- }
- }
-
- val config = PagedList.Config.Builder()
- .setPageSize(10)
- .setEnablePlaceholders(enablePlaceholders)
- .build()
- if (enablePlaceholders) {
- TiledPagedList(dataSource, FailExecutor(), FailExecutor(), null, config, 0)
- } else {
- ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
- FailExecutor(), FailExecutor(), null, config, null,
- ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
- }
- }
-
- @Test
- fun initialLoadCallbackSuccess() = performLoadInitial {
- // LoadInitialCallback correct usage
- it.onResult(listOf("a", "b"), 0, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackNotPageSizeMultiple() = performLoadInitial {
- // Positional LoadInitialCallback can't accept result that's not a multiple of page size
- val elevenLetterList = List(11) { "" + 'a' + it }
- it.onResult(elevenLetterList, 0, 12)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackListTooBig() = performLoadInitial {
- // LoadInitialCallback can't accept pos + list > totalCount
- it.onResult(listOf("a", "b", "c"), 0, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackPositionTooLarge() = performLoadInitial {
- // LoadInitialCallback can't accept pos + list > totalCount
- it.onResult(listOf("a", "b"), 1, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackPositionNegative() = performLoadInitial {
- // LoadInitialCallback can't accept negative position
- it.onResult(listOf("a", "b", "c"), -1, 2)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
- // LoadInitialCallback can't accept empty result unless data set is empty
- it.onResult(emptyList(), 0, 2)
- }
-
- @Test(expected = IllegalStateException::class)
- fun initialLoadCallbackRequireTotalCount() = performLoadInitial(enablePlaceholders = true) {
- // LoadInitialCallback requires 3 args when placeholders enabled
- it.onResult(listOf("a", "b"), 0)
- }
-
- @Test
- fun initialLoadCallbackSuccessTwoArg() = performLoadInitial(enablePlaceholders = false) {
- // LoadInitialCallback correct 2 arg usage
- it.onResult(listOf("a", "b"), 0)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackPosNegativeTwoArg() = performLoadInitial(enablePlaceholders = false) {
- // LoadInitialCallback can't accept negative position
- it.onResult(listOf("a", "b"), -1)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun initialLoadCallbackEmptyWithOffset() = performLoadInitial(enablePlaceholders = false) {
- // LoadInitialCallback can't accept empty result unless pos is 0
- it.onResult(emptyList(), 1)
- }
-
- @Test
- fun initialLoadCallbackInvalidTwoArg() = performLoadInitial(invalidateDataSource = true) {
- // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
- it.onResult(emptyList(), 1)
- }
-
- @Test
- fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
- // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
- it.onResult(emptyList(), 0, 1)
- }
-
- private abstract class WrapperDataSource<in A, B>(private val source: PositionalDataSource<A>)
- : PositionalDataSource<B>() {
- private val invalidatedCallback = DataSource.InvalidatedCallback {
- invalidate()
- removeCallback()
- }
-
- init {
- source.addInvalidatedCallback(invalidatedCallback)
- }
-
- private fun removeCallback() {
- removeInvalidatedCallback(invalidatedCallback)
- }
-
- override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<B>) {
- source.loadInitial(params, object : LoadInitialCallback<A>() {
- override fun onResult(data: List<A>, position: Int, totalCount: Int) {
- callback.onResult(convert(data), position, totalCount)
- }
-
- override fun onResult(data: List<A>, position: Int) {
- callback.onResult(convert(data), position)
- }
- })
- }
-
- override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<B>) {
- source.loadRange(params, object : LoadRangeCallback<A>() {
- override fun onResult(data: List<A>) {
- callback.onResult(convert(data))
- }
- })
- }
-
- protected abstract fun convert(source: List<A>): List<B>
- }
-
- private class StringWrapperDataSource<in A>(source: PositionalDataSource<A>)
- : WrapperDataSource<A, String>(source) {
- override fun convert(source: List<A>): List<String> {
- return source.map { it.toString() }
- }
- }
-
- private fun verifyWrappedDataSource(
- createWrapper: (PositionalDataSource<Int>) -> PositionalDataSource<String>) {
- val orig = ListDataSource(listOf(0, 5, 4, 8, 12))
- val wrapper = createWrapper(orig)
-
- // load initial
- @Suppress("UNCHECKED_CAST")
- val loadInitialCallback = mock(PositionalDataSource.LoadInitialCallback::class.java)
- as PositionalDataSource.LoadInitialCallback<String>
-
- wrapper.loadInitial(PositionalDataSource.LoadInitialParams(0, 2, 1, true),
- loadInitialCallback)
- verify(loadInitialCallback).onResult(listOf("0", "5"), 0, 5)
- verifyNoMoreInteractions(loadInitialCallback)
-
- // load range
- @Suppress("UNCHECKED_CAST")
- val loadRangeCallback = mock(PositionalDataSource.LoadRangeCallback::class.java)
- as PositionalDataSource.LoadRangeCallback<String>
-
- wrapper.loadRange(PositionalDataSource.LoadRangeParams(2, 3), loadRangeCallback)
- verify(loadRangeCallback).onResult(listOf("4", "8", "12"))
- verifyNoMoreInteractions(loadRangeCallback)
-
- // check invalidation behavior
- val invalCallback = mock(DataSource.InvalidatedCallback::class.java)
- wrapper.addInvalidatedCallback(invalCallback)
- orig.invalidate()
- verify(invalCallback).onInvalidated()
- verifyNoMoreInteractions(invalCallback)
- }
-
- @Test
- fun testManualWrappedDataSource() = verifyWrappedDataSource {
- StringWrapperDataSource(it)
- }
-
- @Test
- fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
- it.mapByPage { it.map { it.toString() } }
- }
-
- @Test
- fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
- it.map { it.toString() }
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt b/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
deleted file mode 100644
index f41e6a6..0000000
--- a/paging/common/src/test/java/android/arch/paging/TiledDataSourceTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 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.paging
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import java.util.Collections
-
-@Suppress("DEPRECATION")
-@RunWith(JUnit4::class)
-class TiledDataSourceTest {
-
- fun TiledDataSource<String>.loadInitial(
- startPosition: Int, count: Int, pageSize: Int): List<String> {
- @Suppress("UNCHECKED_CAST")
- val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<String>
-
- this.dispatchLoadInitial(true, startPosition, count, pageSize, FailExecutor(), receiver)
-
- @Suppress("UNCHECKED_CAST")
- val argument = ArgumentCaptor.forClass(PageResult::class.java)
- as ArgumentCaptor<PageResult<String>>
- verify(receiver).onPageResult(eq(PageResult.INIT), argument.capture())
- verifyNoMoreInteractions(receiver)
-
- val observed = argument.value
-
- return observed.page
- }
-
- @Test
- fun loadInitialEmpty() {
- class EmptyDataSource : TiledDataSource<String>() {
- override fun countItems(): Int {
- return 0
- }
-
- override fun loadRange(startPosition: Int, count: Int): List<String> {
- return emptyList()
- }
- }
-
- assertEquals(Collections.EMPTY_LIST, EmptyDataSource().loadInitial(0, 1, 5))
- }
-
- @Test
- fun loadInitialTooLong() {
- val list = List(26) { "" + 'a' + it }
- class AlphabetDataSource : TiledDataSource<String>() {
- override fun countItems(): Int {
- return list.size
- }
-
- override fun loadRange(startPosition: Int, count: Int): List<String> {
- return list.subList(startPosition, startPosition + count)
- }
- }
- // baseline behavior
- assertEquals(list, AlphabetDataSource().loadInitial(0, 26, 10))
- assertEquals(list, AlphabetDataSource().loadInitial(50, 26, 10))
- }
-}
diff --git a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt b/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
deleted file mode 100644
index fbd8f54..0000000
--- a/paging/common/src/test/java/android/arch/paging/TiledPagedListTest.kt
+++ /dev/null
@@ -1,425 +0,0 @@
-/*
- * 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.paging
-
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
-
-@RunWith(JUnit4::class)
-class TiledPagedListTest {
- private val mMainThread = TestExecutor()
- private val mBackgroundThread = TestExecutor()
-
- private class Item(position: Int) {
- val name: String = "Item $position"
-
- override fun toString(): String {
- return name
- }
- }
-
- private fun verifyLoadedPages(list: List<Item>, vararg loadedPages: Int) {
- val loadedPageList = loadedPages.asList()
- assertEquals(ITEMS.size, list.size)
- for (i in list.indices) {
- if (loadedPageList.contains(i / PAGE_SIZE)) {
- assertSame("Index $i", ITEMS[i], list[i])
- } else {
- assertNull("Index $i", list[i])
- }
- }
- }
-
- private fun createTiledPagedList(loadPosition: Int, initPageCount: Int,
- prefetchDistance: Int = PAGE_SIZE,
- listData: List<Item> = ITEMS,
- boundaryCallback: PagedList.BoundaryCallback<Item>? = null): TiledPagedList<Item> {
- return TiledPagedList(
- ListDataSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
- PagedList.Config.Builder()
- .setPageSize(PAGE_SIZE)
- .setInitialLoadSizeHint(PAGE_SIZE * initPageCount)
- .setPrefetchDistance(prefetchDistance)
- .build(),
- loadPosition)
- }
-
- @Test
- fun getDataSource() {
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
- assertTrue(pagedList.dataSource is ListDataSource<Item>)
-
- // snapshot keeps same DataSource
- assertSame(pagedList.dataSource,
- (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource)
- }
-
- @Test
- fun initialLoad_onePage() {
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
- verifyLoadedPages(pagedList, 0, 1)
- }
-
- @Test
- fun initialLoad_onePageOffset() {
- val pagedList = createTiledPagedList(loadPosition = 10, initPageCount = 1)
- verifyLoadedPages(pagedList, 0, 1)
- }
-
- @Test
- fun initialLoad_full() {
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 100)
- verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
- }
-
- @Test
- fun initialLoad_end() {
- val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
- verifyLoadedPages(pagedList, 3, 4)
- }
-
- @Test
- fun initialLoad_multiple() {
- val pagedList = createTiledPagedList(loadPosition = 9, initPageCount = 2)
- verifyLoadedPages(pagedList, 0, 1)
- }
-
- @Test
- fun initialLoad_offset() {
- val pagedList = createTiledPagedList(loadPosition = 41, initPageCount = 2)
- verifyLoadedPages(pagedList, 3, 4)
- }
-
- @Test
- fun initialLoad_initializesLastKey() {
- val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
- assertEquals(44, pagedList.lastKey)
- }
-
- @Test
- fun initialLoadAsync() {
- val dataSource = AsyncListDataSource(ITEMS)
- val pagedList = TiledPagedList(
- dataSource, mMainThread, mBackgroundThread, null,
- PagedList.Config.Builder().setPageSize(10).build(), 0)
-
- assertTrue(pagedList.isEmpty())
- drain()
- assertTrue(pagedList.isEmpty())
- dataSource.flush()
- assertTrue(pagedList.isEmpty())
- mBackgroundThread.executeAll()
- assertTrue(pagedList.isEmpty())
-
- // Data source defers callbacks until flush, which posts result to main thread
- mMainThread.executeAll()
- assertFalse(pagedList.isEmpty())
- }
-
- @Test
- fun addWeakCallbackEmpty() {
- val dataSource = AsyncListDataSource(ITEMS)
- val pagedList = TiledPagedList(
- dataSource, mMainThread, mBackgroundThread, null,
- PagedList.Config.Builder().setPageSize(10).build(), 0)
-
- // capture empty snapshot
- val emptySnapshot = pagedList.snapshot()
- assertTrue(pagedList.isEmpty())
- assertTrue(emptySnapshot.isEmpty())
-
- // data added in asynchronously
- dataSource.flush()
- drain()
- assertFalse(pagedList.isEmpty())
-
- // verify that adding callback works with empty start point
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(emptySnapshot, callback)
- verify(callback).onInserted(0, pagedList.size)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun append() {
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyLoadedPages(pagedList, 0, 1)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(15)
-
- verifyLoadedPages(pagedList, 0, 1)
-
- drain()
-
- verifyLoadedPages(pagedList, 0, 1, 2)
- verify(callback).onChanged(20, 10)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prepend() {
- val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyLoadedPages(pagedList, 3, 4)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(35)
- drain()
-
- verifyLoadedPages(pagedList, 2, 3, 4)
- verify<PagedList.Callback>(callback).onChanged(20, 10)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun loadWithGap() {
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyLoadedPages(pagedList, 0, 1)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(44)
- drain()
-
- verifyLoadedPages(pagedList, 0, 1, 3, 4)
- verify(callback).onChanged(30, 10)
- verify(callback).onChanged(40, 5)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun tinyPrefetchTest() {
- val pagedList = createTiledPagedList(
- loadPosition = 0, initPageCount = 1, prefetchDistance = 1)
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(null, callback)
- verifyLoadedPages(pagedList, 0, 1)
- verifyZeroInteractions(callback)
-
- pagedList.loadAround(33)
- drain()
-
- verifyLoadedPages(pagedList, 0, 1, 3)
- verify(callback).onChanged(30, 10)
- verifyNoMoreInteractions(callback)
-
- pagedList.loadAround(44)
- drain()
-
- verifyLoadedPages(pagedList, 0, 1, 3, 4)
- verify(callback).onChanged(40, 5)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun appendCallbackAddedLate() {
- val pagedList = createTiledPagedList(
- loadPosition = 0, initPageCount = 1, prefetchDistance = 0)
- verifyLoadedPages(pagedList, 0, 1)
-
- pagedList.loadAround(25)
- drain()
- verifyLoadedPages(pagedList, 0, 1, 2)
-
- // snapshot at 30 items
- val snapshot = pagedList.snapshot()
- verifyLoadedPages(snapshot, 0, 1, 2)
-
- pagedList.loadAround(35)
- pagedList.loadAround(44)
- drain()
- verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
- verifyLoadedPages(snapshot, 0, 1, 2)
-
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(snapshot, callback)
- verify(callback).onChanged(30, 20)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun prependCallbackAddedLate() {
- val pagedList = createTiledPagedList(
- loadPosition = 44, initPageCount = 2, prefetchDistance = 0)
- verifyLoadedPages(pagedList, 3, 4)
-
- pagedList.loadAround(25)
- drain()
- verifyLoadedPages(pagedList, 2, 3, 4)
-
- // snapshot at 30 items
- val snapshot = pagedList.snapshot()
- verifyLoadedPages(snapshot, 2, 3, 4)
-
- pagedList.loadAround(15)
- pagedList.loadAround(5)
- drain()
- verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
- verifyLoadedPages(snapshot, 2, 3, 4)
-
- val callback = mock(PagedList.Callback::class.java)
- pagedList.addWeakCallback(snapshot, callback)
- verify(callback).onChanged(0, 20)
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun placeholdersDisabled() {
- // disable placeholders with config, so we create a contiguous version of the pagedlist
- val config = PagedList.Config.Builder()
- .setPageSize(PAGE_SIZE)
- .setPrefetchDistance(PAGE_SIZE)
- .setInitialLoadSizeHint(PAGE_SIZE)
- .setEnablePlaceholders(false)
- .build()
- val pagedList = PagedList.Builder<Int, Item>(ListDataSource(ITEMS), config)
- .setMainThreadExecutor(mMainThread)
- .setBackgroundThreadExecutor(mBackgroundThread)
- .setInitialKey(20)
- .build()
-
- assertTrue(pagedList.isContiguous)
-
- @Suppress("UNCHECKED_CAST")
- val contiguousPagedList = pagedList as ContiguousPagedList<Int, Item>
- assertEquals(0, contiguousPagedList.mStorage.leadingNullCount)
- assertEquals(PAGE_SIZE, contiguousPagedList.mStorage.storageCount)
- assertEquals(0, contiguousPagedList.mStorage.trailingNullCount)
- }
-
- @Test
- fun boundaryCallback_empty() {
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
- listData = ArrayList(), boundaryCallback = boundaryCallback)
- assertEquals(0, pagedList.size)
-
- // nothing yet
- verifyNoMoreInteractions(boundaryCallback)
-
- // onZeroItemsLoaded posted, since creation often happens on BG thread
- drain()
- verify(boundaryCallback).onZeroItemsLoaded()
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- @Test
- fun boundaryCallback_immediate() {
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
- listData = ITEMS.subList(0, 2), boundaryCallback = boundaryCallback)
- assertEquals(2, pagedList.size)
-
- // nothing yet
- verifyZeroInteractions(boundaryCallback)
-
- // callbacks posted, since creation often happens on BG thread
- drain()
- verify(boundaryCallback).onItemAtFrontLoaded(ITEMS[0])
- verify(boundaryCallback).onItemAtEndLoaded(ITEMS[1])
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- @Test
- fun boundaryCallback_delayedUntilLoaded() {
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createTiledPagedList(loadPosition = 20, initPageCount = 1,
- boundaryCallback = boundaryCallback)
- verifyLoadedPages(pagedList, 1, 2) // 0, 3, and 4 not loaded yet
-
- // nothing yet, even after drain
- verifyZeroInteractions(boundaryCallback)
- drain()
- verifyZeroInteractions(boundaryCallback)
-
- pagedList.loadAround(0)
- pagedList.loadAround(44)
-
- // still nothing, since items aren't loaded...
- verifyZeroInteractions(boundaryCallback)
-
- drain()
- // first/last items loaded now, so callbacks dispatched
- verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
- verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- @Test
- fun boundaryCallback_delayedUntilNearbyAccess() {
- @Suppress("UNCHECKED_CAST")
- val boundaryCallback =
- mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
- val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 5,
- prefetchDistance = 2, boundaryCallback = boundaryCallback)
- verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
-
- // all items loaded, but no access near ends, so no callbacks
- verifyZeroInteractions(boundaryCallback)
- drain()
- verifyZeroInteractions(boundaryCallback)
-
- pagedList.loadAround(0)
- pagedList.loadAround(44)
-
- // callbacks not posted immediately
- verifyZeroInteractions(boundaryCallback)
-
- drain()
-
- // items accessed, so now posted callbacks are run
- verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
- verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
- verifyNoMoreInteractions(boundaryCallback)
- }
-
- private fun drain() {
- var executed: Boolean
- do {
- executed = mBackgroundThread.executeAll()
- executed = mMainThread.executeAll() || executed
- } while (executed)
- }
-
- companion object {
- // use a page size that's not an even divisor of ITEMS.size() to test end conditions
- private val PAGE_SIZE = 10
-
- private val ITEMS = List(45) { Item(it) }
- }
-}
diff --git a/paging/common/src/test/java/androidx/paging/AsyncListDataSource.kt b/paging/common/src/test/java/androidx/paging/AsyncListDataSource.kt
new file mode 100644
index 0000000..6a4ff37
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/AsyncListDataSource.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 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 androidx.paging
+
+class AsyncListDataSource<T>(list: List<T>)
+ : PositionalDataSource<T>() {
+ private val workItems: MutableList<() -> Unit> = ArrayList()
+ private val listDataSource = ListDataSource(list)
+
+ override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
+ workItems.add {
+ listDataSource.loadInitial(params, callback)
+ }
+ }
+
+ override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
+ workItems.add {
+ listDataSource.loadRange(params, callback)
+ }
+ }
+
+ fun flush() {
+ workItems.map { it() }
+ workItems.clear()
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt b/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt
new file mode 100644
index 0000000..2a2b8a4
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/ContiguousPagedListTest.kt
@@ -0,0 +1,519 @@
+/*
+ * 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 androidx.paging
+
+import androidx.arch.core.util.Function
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+import java.util.concurrent.Executor
+
+@RunWith(Parameterized::class)
+class ContiguousPagedListTest(private val mCounted: Boolean) {
+ private val mMainThread = TestExecutor()
+ private val mBackgroundThread = TestExecutor()
+
+ private class Item(position: Int) {
+ val name: String = "Item $position"
+
+ override fun toString(): String {
+ return name
+ }
+ }
+
+ private inner class TestSource(val listData: List<Item> = ITEMS)
+ : ContiguousDataSource<Int, Item>() {
+ override fun dispatchLoadInitial(
+ key: Int?,
+ initialLoadSize: Int,
+ pageSize: Int,
+ enablePlaceholders: Boolean,
+ mainThreadExecutor: Executor,
+ receiver: PageResult.Receiver<Item>) {
+
+ val convertPosition = key ?: 0
+ val position = Math.max(0, (convertPosition - initialLoadSize / 2))
+ val data = getClampedRange(position, position + initialLoadSize)
+ val trailingUnloadedCount = listData.size - position - data.size
+
+ if (enablePlaceholders && mCounted) {
+ receiver.onPageResult(PageResult.INIT,
+ PageResult(data, position, trailingUnloadedCount, 0))
+ } else {
+ // still must pass offset, even if not counted
+ receiver.onPageResult(PageResult.INIT,
+ PageResult(data, position))
+ }
+ }
+
+ override fun dispatchLoadAfter(
+ currentEndIndex: Int,
+ currentEndItem: Item,
+ pageSize: Int,
+ mainThreadExecutor: Executor,
+ receiver: PageResult.Receiver<Item>) {
+ val startIndex = currentEndIndex + 1
+ val data = getClampedRange(startIndex, startIndex + pageSize)
+
+ mainThreadExecutor.execute {
+ receiver.onPageResult(PageResult.APPEND, PageResult(data, 0, 0, 0))
+ }
+ }
+
+ override fun dispatchLoadBefore(
+ currentBeginIndex: Int,
+ currentBeginItem: Item,
+ pageSize: Int,
+ mainThreadExecutor: Executor,
+ receiver: PageResult.Receiver<Item>) {
+
+ val startIndex = currentBeginIndex - 1
+ val data = getClampedRange(startIndex - pageSize + 1, startIndex + 1)
+
+ mainThreadExecutor.execute {
+ receiver.onPageResult(PageResult.PREPEND, PageResult(data, 0, 0, 0))
+ }
+ }
+
+ override fun getKey(position: Int, item: Item?): Int {
+ return 0
+ }
+
+ private fun getClampedRange(startInc: Int, endExc: Int): List<Item> {
+ return listData.subList(Math.max(0, startInc), Math.min(listData.size, endExc))
+ }
+
+ override fun <ToValue : Any?> mapByPage(function: Function<List<Item>, List<ToValue>>):
+ DataSource<Int, ToValue> {
+ throw UnsupportedOperationException()
+ }
+
+ override fun <ToValue : Any?> map(function: Function<Item, ToValue>):
+ DataSource<Int, ToValue> {
+ throw UnsupportedOperationException()
+ }
+ }
+
+ private fun verifyRange(start: Int, count: Int, actual: PagedStorage<Item>) {
+ if (mCounted) {
+ // assert nulls + content
+ val expected = arrayOfNulls<Item>(ITEMS.size)
+ System.arraycopy(ITEMS.toTypedArray(), start, expected, start, count)
+ assertArrayEquals(expected, actual.toTypedArray())
+
+ val expectedTrailing = ITEMS.size - start - count
+ assertEquals(ITEMS.size, actual.size)
+ assertEquals((ITEMS.size - start - expectedTrailing),
+ actual.storageCount)
+ assertEquals(start, actual.leadingNullCount)
+ assertEquals(expectedTrailing, actual.trailingNullCount)
+ } else {
+ assertEquals(ITEMS.subList(start, start + count), actual)
+
+ assertEquals(count, actual.size)
+ assertEquals(actual.size, actual.storageCount)
+ assertEquals(0, actual.leadingNullCount)
+ assertEquals(0, actual.trailingNullCount)
+ }
+ }
+
+ private fun verifyRange(start: Int, count: Int, actual: PagedList<Item>) {
+ verifyRange(start, count, actual.mStorage)
+ }
+
+ private fun createCountedPagedList(
+ initialPosition: Int,
+ pageSize: Int = 20,
+ initLoadSize: Int = 40,
+ prefetchDistance: Int = 20,
+ listData: List<Item> = ITEMS,
+ boundaryCallback: PagedList.BoundaryCallback<Item>? = null,
+ lastLoad: Int = ContiguousPagedList.LAST_LOAD_UNSPECIFIED
+ ): ContiguousPagedList<Int, Item> {
+ return ContiguousPagedList(
+ TestSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
+ PagedList.Config.Builder()
+ .setInitialLoadSizeHint(initLoadSize)
+ .setPageSize(pageSize)
+ .setPrefetchDistance(prefetchDistance)
+ .build(),
+ initialPosition,
+ lastLoad)
+ }
+
+ @Test
+ fun construct() {
+ val pagedList = createCountedPagedList(0)
+ verifyRange(0, 40, pagedList)
+ }
+
+ @Test
+ fun getDataSource() {
+ val pagedList = createCountedPagedList(0)
+ assertTrue(pagedList.dataSource is TestSource)
+
+ // snapshot keeps same DataSource
+ assertSame(pagedList.dataSource,
+ (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource)
+ }
+
+ private fun verifyCallback(callback: PagedList.Callback, countedPosition: Int,
+ uncountedPosition: Int) {
+ if (mCounted) {
+ verify(callback).onChanged(countedPosition, 20)
+ } else {
+ verify(callback).onInserted(uncountedPosition, 20)
+ }
+ }
+
+ @Test
+ fun append() {
+ val pagedList = createCountedPagedList(0)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyRange(0, 40, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(35)
+ drain()
+
+ verifyRange(0, 60, pagedList)
+ verifyCallback(callback, 40, 40)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prepend() {
+ val pagedList = createCountedPagedList(80)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyRange(60, 40, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(if (mCounted) 65 else 5)
+ drain()
+
+ verifyRange(40, 60, pagedList)
+ verifyCallback(callback, 40, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun outwards() {
+ val pagedList = createCountedPagedList(50)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyRange(30, 40, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(if (mCounted) 65 else 35)
+ drain()
+
+ verifyRange(30, 60, pagedList)
+ verifyCallback(callback, 70, 40)
+ verifyNoMoreInteractions(callback)
+
+ pagedList.loadAround(if (mCounted) 35 else 5)
+ drain()
+
+ verifyRange(10, 80, pagedList)
+ verifyCallback(callback, 10, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun multiAppend() {
+ val pagedList = createCountedPagedList(0)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyRange(0, 40, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(55)
+ drain()
+
+ verifyRange(0, 80, pagedList)
+ verifyCallback(callback, 40, 40)
+ verifyCallback(callback, 60, 60)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun distantPrefetch() {
+ val pagedList = createCountedPagedList(0,
+ initLoadSize = 10, pageSize = 10, prefetchDistance = 30)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyRange(0, 10, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(5)
+ drain()
+
+ verifyRange(0, 40, pagedList)
+
+ pagedList.loadAround(6)
+ drain()
+
+ // although our prefetch window moves forward, no new load triggered
+ verifyRange(0, 40, pagedList)
+ }
+
+ @Test
+ fun appendCallbackAddedLate() {
+ val pagedList = createCountedPagedList(0)
+ verifyRange(0, 40, pagedList)
+
+ pagedList.loadAround(35)
+ drain()
+ verifyRange(0, 60, pagedList)
+
+ // snapshot at 60 items
+ val snapshot = pagedList.snapshot() as PagedList<Item>
+ verifyRange(0, 60, snapshot)
+
+ // load more items...
+ pagedList.loadAround(55)
+ drain()
+ verifyRange(0, 80, pagedList)
+ verifyRange(0, 60, snapshot)
+
+ // and verify the snapshot hasn't received them
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(snapshot, callback)
+ verifyCallback(callback, 60, 60)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependCallbackAddedLate() {
+ val pagedList = createCountedPagedList(80)
+ verifyRange(60, 40, pagedList)
+
+ pagedList.loadAround(if (mCounted) 65 else 5)
+ drain()
+ verifyRange(40, 60, pagedList)
+
+ // snapshot at 60 items
+ val snapshot = pagedList.snapshot() as PagedList<Item>
+ verifyRange(40, 60, snapshot)
+
+ pagedList.loadAround(if (mCounted) 45 else 5)
+ drain()
+ verifyRange(20, 80, pagedList)
+ verifyRange(40, 60, snapshot)
+
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(snapshot, callback)
+ verifyCallback(callback, 40, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun initialLoad_lastLoad() {
+ val pagedList = createCountedPagedList(
+ initialPosition = 0,
+ initLoadSize = 20,
+ lastLoad = 4)
+ // last load is param passed
+ assertEquals(4, pagedList.mLastLoad)
+ verifyRange(0, 20, pagedList)
+ }
+
+ @Test
+ fun initialLoad_lastLoadComputed() {
+ val pagedList = createCountedPagedList(
+ initialPosition = 0,
+ initLoadSize = 20,
+ lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ // last load is middle of initial load
+ assertEquals(10, pagedList.mLastLoad)
+ verifyRange(0, 20, pagedList)
+ }
+
+ @Test
+ fun initialLoadAsync() {
+ // Note: ignores Parameterized param
+ val asyncDataSource = AsyncListDataSource(ITEMS)
+ val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders()
+ val pagedList = ContiguousPagedList(
+ dataSource, mMainThread, mBackgroundThread, null,
+ PagedList.Config.Builder().setPageSize(10).build(), null,
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+
+ assertTrue(pagedList.isEmpty())
+ drain()
+ assertTrue(pagedList.isEmpty())
+ asyncDataSource.flush()
+ assertTrue(pagedList.isEmpty())
+ mBackgroundThread.executeAll()
+ assertTrue(pagedList.isEmpty())
+ verifyZeroInteractions(callback)
+
+ // Data source defers callbacks until flush, which posts result to main thread
+ mMainThread.executeAll()
+ assertFalse(pagedList.isEmpty())
+ // callback onInsert called once with initial size
+ verify(callback).onInserted(0, pagedList.size)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun addWeakCallbackEmpty() {
+ // Note: ignores Parameterized param
+ val asyncDataSource = AsyncListDataSource(ITEMS)
+ val dataSource = asyncDataSource.wrapAsContiguousWithoutPlaceholders()
+ val pagedList = ContiguousPagedList(
+ dataSource, mMainThread, mBackgroundThread, null,
+ PagedList.Config.Builder().setPageSize(10).build(), null,
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ val callback = mock(PagedList.Callback::class.java)
+
+ // capture empty snapshot
+ val emptySnapshot = pagedList.snapshot()
+ assertTrue(pagedList.isEmpty())
+ assertTrue(emptySnapshot.isEmpty())
+
+ // verify that adding callback notifies nothing going from empty -> empty
+ pagedList.addWeakCallback(emptySnapshot, callback)
+ verifyZeroInteractions(callback)
+ pagedList.removeWeakCallback(callback)
+
+ // data added in asynchronously
+ asyncDataSource.flush()
+ drain()
+ assertFalse(pagedList.isEmpty())
+
+ // verify that adding callback notifies insert going from empty -> content
+ pagedList.addWeakCallback(emptySnapshot, callback)
+ verify(callback).onInserted(0, pagedList.size)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun boundaryCallback_empty() {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createCountedPagedList(0,
+ listData = ArrayList(), boundaryCallback = boundaryCallback)
+ assertEquals(0, pagedList.size)
+
+ // nothing yet
+ verifyNoMoreInteractions(boundaryCallback)
+
+ // onZeroItemsLoaded posted, since creation often happens on BG thread
+ drain()
+ verify(boundaryCallback).onZeroItemsLoaded()
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun boundaryCallback_singleInitialLoad() {
+ val shortList = ITEMS.subList(0, 4)
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createCountedPagedList(0, listData = shortList,
+ initLoadSize = shortList.size, boundaryCallback = boundaryCallback)
+ assertEquals(shortList.size, pagedList.size)
+
+ // nothing yet
+ verifyNoMoreInteractions(boundaryCallback)
+
+ // onItemAtFrontLoaded / onItemAtEndLoaded posted, since creation often happens on BG thread
+ drain()
+ pagedList.loadAround(0)
+ drain()
+ verify(boundaryCallback).onItemAtFrontLoaded(shortList.first())
+ verify(boundaryCallback).onItemAtEndLoaded(shortList.last())
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun boundaryCallback_delayed() {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createCountedPagedList(90,
+ initLoadSize = 20, prefetchDistance = 5, boundaryCallback = boundaryCallback)
+ verifyRange(80, 20, pagedList)
+
+ // nothing yet
+ verifyZeroInteractions(boundaryCallback)
+ drain()
+ verifyZeroInteractions(boundaryCallback)
+
+ // loading around last item causes onItemAtEndLoaded
+ pagedList.loadAround(if (mCounted) 99 else 19)
+ drain()
+ verifyRange(80, 20, pagedList)
+ verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
+ verifyNoMoreInteractions(boundaryCallback)
+
+ // prepending doesn't trigger callback...
+ pagedList.loadAround(if (mCounted) 80 else 0)
+ drain()
+ verifyRange(60, 40, pagedList)
+ verifyZeroInteractions(boundaryCallback)
+
+ // ...load rest of data, still no dispatch...
+ pagedList.loadAround(if (mCounted) 60 else 0)
+ drain()
+ pagedList.loadAround(if (mCounted) 40 else 0)
+ drain()
+ pagedList.loadAround(if (mCounted) 20 else 0)
+ drain()
+ verifyRange(0, 100, pagedList)
+ verifyZeroInteractions(boundaryCallback)
+
+ // ... finally try prepend, see 0 items, which will dispatch front callback
+ pagedList.loadAround(0)
+ drain()
+ verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ private fun drain() {
+ var executed: Boolean
+ do {
+ executed = mBackgroundThread.executeAll()
+ executed = mMainThread.executeAll() || executed
+ } while (executed)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "counted:{0}")
+ fun parameters(): Array<Array<Boolean>> {
+ return arrayOf(arrayOf(true), arrayOf(false))
+ }
+
+ private val ITEMS = List(100) { Item(it) }
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/Executors.kt b/paging/common/src/test/java/androidx/paging/Executors.kt
new file mode 100644
index 0000000..16458bc
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/Executors.kt
@@ -0,0 +1,46 @@
+/*
+ * 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 androidx.paging
+
+import org.junit.Assert.fail
+import java.util.LinkedList
+import java.util.concurrent.Executor
+
+class TestExecutor : Executor {
+ private val mTasks = LinkedList<Runnable>()
+
+ override fun execute(runnable: Runnable) {
+ mTasks.add(runnable)
+ }
+
+ internal fun executeAll(): Boolean {
+ val consumed = !mTasks.isEmpty()
+
+ var task = mTasks.poll()
+ while (task != null) {
+ task.run()
+ task = mTasks.poll()
+ }
+ return consumed
+ }
+}
+
+class FailExecutor(val string: String = "Executor expected to be unused") : Executor {
+ override fun execute(runnable: Runnable?) {
+ fail(string)
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt
new file mode 100644
index 0000000..bdb001a
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/ItemKeyedDataSourceTest.kt
@@ -0,0 +1,457 @@
+/*
+ * 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 androidx.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(JUnit4::class)
+class ItemKeyedDataSourceTest {
+
+ // ----- STANDARD -----
+
+ private fun loadInitial(dataSource: ItemDataSource, key: Key?, initialLoadSize: Int,
+ enablePlaceholders: Boolean): PageResult<Item> {
+ @Suppress("UNCHECKED_CAST")
+ val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<Item>
+ @Suppress("UNCHECKED_CAST")
+ val captor = ArgumentCaptor.forClass(PageResult::class.java)
+ as ArgumentCaptor<PageResult<Item>>
+
+ dataSource.dispatchLoadInitial(key, initialLoadSize,
+ /* ignored pageSize */ 10, enablePlaceholders, FailExecutor(), receiver)
+
+ verify(receiver).onPageResult(anyInt(), captor.capture())
+ verifyNoMoreInteractions(receiver)
+ assertNotNull(captor.value)
+ return captor.value
+ }
+
+ @Test
+ fun loadInitial() {
+ val dataSource = ItemDataSource()
+ val result = loadInitial(dataSource, dataSource.getKey(ITEMS_BY_NAME_ID[49]), 10, true)
+
+ assertEquals(45, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
+ assertEquals(45, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_keyMatchesSingleItem() {
+ val dataSource = ItemDataSource(items = ITEMS_BY_NAME_ID.subList(0, 1))
+
+ // this is tricky, since load after and load before with the passed key will fail
+ val result = loadInitial(dataSource, dataSource.getKey(ITEMS_BY_NAME_ID[0]), 20, true)
+
+ assertEquals(0, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(0, 1), result.page)
+ assertEquals(0, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_keyMatchesLastItem() {
+ val dataSource = ItemDataSource()
+
+ // tricky, because load after key is empty, so another load before and load after required
+ val key = dataSource.getKey(ITEMS_BY_NAME_ID.last())
+ val result = loadInitial(dataSource, key, 20, true)
+
+ assertEquals(90, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(90, 100), result.page)
+ assertEquals(0, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_nullKey() {
+ val dataSource = ItemDataSource()
+
+ // dispatchLoadInitial(null, count) == dispatchLoadInitial(count)
+ val result = loadInitial(dataSource, null, 10, true)
+
+ assertEquals(0, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
+ assertEquals(90, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_keyPastEndOfList() {
+ val dataSource = ItemDataSource()
+
+ // if key is past entire data set, should return last items in data set
+ val key = Key("fz", 0)
+ val result = loadInitial(dataSource, key, 10, true)
+
+ // NOTE: ideally we'd load 10 items here, but it adds complexity and unpredictability to
+ // do: load after was empty, so pass full size to load before, since this can incur larger
+ // loads than requested (see keyMatchesLastItem test)
+ assertEquals(95, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(95, 100), result.page)
+ assertEquals(0, result.trailingNulls)
+ }
+
+ // ----- UNCOUNTED -----
+
+ @Test
+ fun loadInitial_disablePlaceholders() {
+ val dataSource = ItemDataSource()
+
+ // dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
+ val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
+ val result = loadInitial(dataSource, key, 10, false)
+
+ assertEquals(0, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
+ assertEquals(0, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_uncounted() {
+ val dataSource = ItemDataSource(counted = false)
+
+ // dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
+ val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
+ val result = loadInitial(dataSource, key, 10, true)
+
+ assertEquals(0, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.page)
+ assertEquals(0, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_nullKey_uncounted() {
+ val dataSource = ItemDataSource(counted = false)
+
+ // dispatchLoadInitial(null, count) == dispatchLoadInitial(count)
+ val result = loadInitial(dataSource, null, 10, true)
+
+ assertEquals(0, result.leadingNulls)
+ assertEquals(ITEMS_BY_NAME_ID.subList(0, 10), result.page)
+ assertEquals(0, result.trailingNulls)
+ }
+
+ // ----- EMPTY -----
+
+ @Test
+ fun loadInitial_empty() {
+ val dataSource = ItemDataSource(items = ArrayList())
+
+ // dispatchLoadInitial(key, count) == null padding, loadAfter(key, count), null padding
+ val key = dataSource.getKey(ITEMS_BY_NAME_ID[49])
+ val result = loadInitial(dataSource, key, 10, true)
+
+ assertEquals(0, result.leadingNulls)
+ assertTrue(result.page.isEmpty())
+ assertEquals(0, result.trailingNulls)
+ }
+
+ @Test
+ fun loadInitial_nullKey_empty() {
+ val dataSource = ItemDataSource(items = ArrayList())
+ val result = loadInitial(dataSource, null, 10, true)
+
+ assertEquals(0, result.leadingNulls)
+ assertTrue(result.page.isEmpty())
+ assertEquals(0, result.trailingNulls)
+ }
+
+ // ----- Other behavior -----
+
+ @Test
+ fun loadBefore() {
+ val dataSource = ItemDataSource()
+ @Suppress("UNCHECKED_CAST")
+ val callback = mock(ItemKeyedDataSource.LoadCallback::class.java)
+ as ItemKeyedDataSource.LoadCallback<Item>
+
+ dataSource.loadBefore(
+ ItemKeyedDataSource.LoadParams(dataSource.getKey(ITEMS_BY_NAME_ID[5]), 5), callback)
+
+ @Suppress("UNCHECKED_CAST")
+ val argument = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<Item>>
+ verify(callback).onResult(argument.capture())
+ verifyNoMoreInteractions(callback)
+
+ val observed = argument.value
+
+ assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed)
+ }
+
+ internal data class Key(val name: String, val id: Int)
+
+ internal data class Item(
+ val name: String, val id: Int, val balance: Double, val address: String)
+
+ internal class ItemDataSource(private val counted: Boolean = true,
+ private val items: List<Item> = ITEMS_BY_NAME_ID)
+ : ItemKeyedDataSource<Key, Item>() {
+
+ override fun loadInitial(
+ params: LoadInitialParams<Key>,
+ callback: LoadInitialCallback<Item>) {
+ val key = params.requestedInitialKey ?: Key("", Integer.MAX_VALUE)
+ val start = Math.max(0, findFirstIndexAfter(key) - params.requestedLoadSize / 2)
+ val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
+
+ if (params.placeholdersEnabled && counted) {
+ callback.onResult(items.subList(start, endExclusive), start, items.size)
+ } else {
+ callback.onResult(items.subList(start, endExclusive))
+ }
+ }
+
+ override fun loadAfter(params: LoadParams<Key>, callback: LoadCallback<Item>) {
+ val start = findFirstIndexAfter(params.key)
+ val endExclusive = Math.min(start + params.requestedLoadSize, items.size)
+
+ callback.onResult(items.subList(start, endExclusive))
+ }
+
+ override fun loadBefore(params: LoadParams<Key>, callback: LoadCallback<Item>) {
+ val firstIndexBefore = findFirstIndexBefore(params.key)
+ val endExclusive = Math.max(0, firstIndexBefore + 1)
+ val start = Math.max(0, firstIndexBefore - params.requestedLoadSize + 1)
+
+ callback.onResult(items.subList(start, endExclusive))
+ }
+
+ override fun getKey(item: Item): Key {
+ return Key(item.name, item.id)
+ }
+
+ private fun findFirstIndexAfter(key: Key): Int {
+ return items.indices.firstOrNull {
+ KEY_COMPARATOR.compare(key, getKey(items[it])) < 0
+ } ?: items.size
+ }
+
+ private fun findFirstIndexBefore(key: Key): Int {
+ return items.indices.reversed().firstOrNull {
+ KEY_COMPARATOR.compare(key, getKey(items[it])) > 0
+ } ?: -1
+ }
+ }
+
+ private fun performLoadInitial(
+ invalidateDataSource: Boolean = false,
+ callbackInvoker: (callback: ItemKeyedDataSource.LoadInitialCallback<String>) -> Unit) {
+ val dataSource = object : ItemKeyedDataSource<String, String>() {
+ override fun getKey(item: String): String {
+ return ""
+ }
+
+ override fun loadInitial(
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String>) {
+ if (invalidateDataSource) {
+ // invalidate data source so it's invalid when onResult() called
+ invalidate()
+ }
+ callbackInvoker(callback)
+ }
+
+ override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String>) {
+ fail("loadAfter not expected")
+ }
+
+ override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String>) {
+ fail("loadBefore not expected")
+ }
+ }
+
+ ContiguousPagedList<String, String>(
+ dataSource, FailExecutor(), FailExecutor(), null,
+ PagedList.Config.Builder()
+ .setPageSize(10)
+ .build(),
+ "",
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ }
+
+ @Test
+ fun loadInitialCallbackSuccess() = performLoadInitial {
+ // LoadInitialCallback correct usage
+ it.onResult(listOf("a", "b"), 0, 2)
+ }
+
+ @Test
+ fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
+ // Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
+ val elevenLetterList = List(11) { "" + 'a' + it }
+ it.onResult(elevenLetterList, 0, 12)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackListTooBig() = performLoadInitial {
+ // LoadInitialCallback can't accept pos + list > totalCount
+ it.onResult(listOf("a", "b", "c"), 0, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackPositionTooLarge() = performLoadInitial {
+ // LoadInitialCallback can't accept pos + list > totalCount
+ it.onResult(listOf("a", "b"), 1, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackPositionNegative() = performLoadInitial {
+ // LoadInitialCallback can't accept negative position
+ it.onResult(listOf("a", "b", "c"), -1, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
+ // LoadInitialCallback can't accept empty result unless data set is empty
+ it.onResult(emptyList(), 0, 2)
+ }
+
+ @Test
+ fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
+ // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
+ it.onResult(emptyList(), 0, 1)
+ }
+
+ private abstract class WrapperDataSource<K, A, B>(private val source: ItemKeyedDataSource<K, A>)
+ : ItemKeyedDataSource<K, B>() {
+ private val invalidatedCallback = DataSource.InvalidatedCallback {
+ invalidate()
+ removeCallback()
+ }
+
+ init {
+ source.addInvalidatedCallback(invalidatedCallback)
+ }
+
+ private fun removeCallback() {
+ removeInvalidatedCallback(invalidatedCallback)
+ }
+
+ override fun loadInitial(params: LoadInitialParams<K>, callback: LoadInitialCallback<B>) {
+ source.loadInitial(params, object : LoadInitialCallback<A>() {
+ override fun onResult(data: List<A>, position: Int, totalCount: Int) {
+ callback.onResult(convert(data), position, totalCount)
+ }
+
+ override fun onResult(data: MutableList<A>) {
+ callback.onResult(convert(data))
+ }
+ })
+ }
+
+ override fun loadAfter(params: LoadParams<K>, callback: LoadCallback<B>) {
+ source.loadAfter(params, object : LoadCallback<A>() {
+ override fun onResult(data: MutableList<A>) {
+ callback.onResult(convert(data))
+ }
+ })
+ }
+
+ override fun loadBefore(params: LoadParams<K>, callback: LoadCallback<B>) {
+ source.loadBefore(params, object : LoadCallback<A>() {
+ override fun onResult(data: MutableList<A>) {
+ callback.onResult(convert(data))
+ }
+ })
+ }
+
+ protected abstract fun convert(source: List<A>): List<B>
+ }
+
+ private data class DecoratedItem(val item: Item)
+
+ private class DecoratedWrapperDataSource(private val source: ItemKeyedDataSource<Key, Item>)
+ : WrapperDataSource<Key, Item, DecoratedItem>(source) {
+ override fun convert(source: List<Item>): List<DecoratedItem> {
+ return source.map { DecoratedItem(it) }
+ }
+
+ override fun getKey(item: DecoratedItem): Key {
+ return source.getKey(item.item)
+ }
+ }
+
+ private fun verifyWrappedDataSource(createWrapper:
+ (ItemKeyedDataSource<Key, Item>) -> ItemKeyedDataSource<Key, DecoratedItem>) {
+ // verify that it's possible to wrap an ItemKeyedDataSource, and add info to its data
+
+ val orig = ItemDataSource(items = ITEMS_BY_NAME_ID)
+ val wrapper = DecoratedWrapperDataSource(orig)
+
+ // load initial
+ @Suppress("UNCHECKED_CAST")
+ val loadInitialCallback = mock(ItemKeyedDataSource.LoadInitialCallback::class.java)
+ as ItemKeyedDataSource.LoadInitialCallback<DecoratedItem>
+ val initKey = orig.getKey(ITEMS_BY_NAME_ID.first())
+ wrapper.loadInitial(ItemKeyedDataSource.LoadInitialParams(initKey, 10, false),
+ loadInitialCallback)
+ verify(loadInitialCallback).onResult(
+ ITEMS_BY_NAME_ID.subList(0, 10).map { DecoratedItem(it) })
+ verifyNoMoreInteractions(loadInitialCallback)
+
+ @Suppress("UNCHECKED_CAST")
+ val loadCallback = mock(ItemKeyedDataSource.LoadCallback::class.java)
+ as ItemKeyedDataSource.LoadCallback<DecoratedItem>
+ val key = orig.getKey(ITEMS_BY_NAME_ID[20])
+ // load after
+ wrapper.loadAfter(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
+ verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(21, 31).map { DecoratedItem(it) })
+ verifyNoMoreInteractions(loadCallback)
+
+ // load before
+ wrapper.loadBefore(ItemKeyedDataSource.LoadParams(key, 10), loadCallback)
+ verify(loadCallback).onResult(ITEMS_BY_NAME_ID.subList(10, 20).map { DecoratedItem(it) })
+ verifyNoMoreInteractions(loadCallback)
+ }
+
+ @Test
+ fun testManualWrappedDataSource() = verifyWrappedDataSource {
+ DecoratedWrapperDataSource(it)
+ }
+
+ @Test
+ fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
+ it.mapByPage { it.map { DecoratedItem(it) } }
+ }
+
+ @Test
+ fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
+ it.map { DecoratedItem(it) }
+ }
+
+ companion object {
+ private val ITEM_COMPARATOR = compareBy<Item>({ it.name }).thenByDescending({ it.id })
+ private val KEY_COMPARATOR = compareBy<Key>({ it.name }).thenByDescending({ it.id })
+
+ private val ITEMS_BY_NAME_ID = List(100) {
+ val names = Array(10) { "f" + ('a' + it) }
+ Item(names[it % 10],
+ it,
+ Math.random() * 1000,
+ (Math.random() * 200).toInt().toString() + " fake st.")
+ }.sortedWith(ITEM_COMPARATOR)
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
new file mode 100644
index 0000000..d86b87a
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/PageKeyedDataSourceTest.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright 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 androidx.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(JUnit4::class)
+class PageKeyedDataSourceTest {
+ private val mMainThread = TestExecutor()
+ private val mBackgroundThread = TestExecutor()
+
+ internal data class Item(val name: String)
+
+ internal data class Page(val prev: String?, val data: List<Item>, val next: String?)
+
+ internal class ItemDataSource(val data: Map<String, Page> = PAGE_MAP)
+ : PageKeyedDataSource<String, Item>() {
+
+ private fun getPage(key: String): Page = data[key]!!
+
+ override fun loadInitial(
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String, Item>) {
+ val page = getPage(INIT_KEY)
+ callback.onResult(page.data, page.prev, page.next)
+ }
+
+ override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
+ val page = getPage(params.key)
+ callback.onResult(page.data, page.prev)
+ }
+
+ override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, Item>) {
+ val page = getPage(params.key)
+ callback.onResult(page.data, page.next)
+ }
+ }
+
+ @Test
+ fun loadFullVerify() {
+ // validate paging entire ItemDataSource results in full, correctly ordered data
+ val pagedList = ContiguousPagedList<String, Item>(ItemDataSource(),
+ mMainThread, mBackgroundThread,
+ null, PagedList.Config.Builder().setPageSize(100).build(), null,
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+
+ // validate initial load
+ assertEquals(PAGE_MAP[INIT_KEY]!!.data, pagedList)
+
+ // flush the remaining loads
+ for (i in 0..PAGE_MAP.keys.size) {
+ pagedList.loadAround(0)
+ pagedList.loadAround(pagedList.size - 1)
+ drain()
+ }
+
+ // validate full load
+ assertEquals(ITEM_LIST, pagedList)
+ }
+
+ private fun performLoadInitial(invalidateDataSource: Boolean = false,
+ callbackInvoker:
+ (callback: PageKeyedDataSource.LoadInitialCallback<String, String>) -> Unit) {
+ val dataSource = object : PageKeyedDataSource<String, String>() {
+ override fun loadInitial(
+ params: LoadInitialParams<String>,
+ callback: LoadInitialCallback<String, String>) {
+ if (invalidateDataSource) {
+ // invalidate data source so it's invalid when onResult() called
+ invalidate()
+ }
+ callbackInvoker(callback)
+ }
+
+ override fun loadBefore(
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>) {
+ fail("loadBefore not expected")
+ }
+
+ override fun loadAfter(
+ params: LoadParams<String>,
+ callback: LoadCallback<String, String>) {
+ fail("loadAfter not expected")
+ }
+ }
+
+ ContiguousPagedList<String, String>(
+ dataSource, FailExecutor(), FailExecutor(), null,
+ PagedList.Config.Builder()
+ .setPageSize(10)
+ .build(),
+ "",
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ }
+
+ @Test
+ fun loadInitialCallbackSuccess() = performLoadInitial {
+ // LoadInitialCallback correct usage
+ it.onResult(listOf("a", "b"), 0, 2, null, null)
+ }
+
+ @Test
+ fun loadInitialCallbackNotPageSizeMultiple() = performLoadInitial {
+ // Keyed LoadInitialCallback *can* accept result that's not a multiple of page size
+ val elevenLetterList = List(11) { "" + 'a' + it }
+ it.onResult(elevenLetterList, 0, 12, null, null)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackListTooBig() = performLoadInitial {
+ // LoadInitialCallback can't accept pos + list > totalCount
+ it.onResult(listOf("a", "b", "c"), 0, 2, null, null)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackPositionTooLarge() = performLoadInitial {
+ // LoadInitialCallback can't accept pos + list > totalCount
+ it.onResult(listOf("a", "b"), 1, 2, null, null)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackPositionNegative() = performLoadInitial {
+ // LoadInitialCallback can't accept negative position
+ it.onResult(listOf("a", "b", "c"), -1, 2, null, null)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun loadInitialCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
+ // LoadInitialCallback can't accept empty result unless data set is empty
+ it.onResult(emptyList(), 0, 2, null, null)
+ }
+
+ @Test
+ fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
+ // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
+ it.onResult(emptyList(), 0, 1, null, null)
+ }
+
+ private abstract class WrapperDataSource<K, A, B>(private val source: PageKeyedDataSource<K, A>)
+ : PageKeyedDataSource<K, B>() {
+ private val invalidatedCallback = DataSource.InvalidatedCallback {
+ invalidate()
+ removeCallback()
+ }
+
+ init {
+ source.addInvalidatedCallback(invalidatedCallback)
+ }
+
+ private fun removeCallback() {
+ removeInvalidatedCallback(invalidatedCallback)
+ }
+
+ override fun loadInitial(params: LoadInitialParams<K>,
+ callback: LoadInitialCallback<K, B>) {
+ source.loadInitial(params, object : LoadInitialCallback<K, A>() {
+ override fun onResult(data: List<A>, position: Int, totalCount: Int,
+ previousPageKey: K?, nextPageKey: K?) {
+ callback.onResult(convert(data), position, totalCount,
+ previousPageKey, nextPageKey)
+ }
+
+ override fun onResult(data: MutableList<A>, previousPageKey: K?, nextPageKey: K?) {
+ callback.onResult(convert(data), previousPageKey, nextPageKey)
+ }
+ })
+ }
+
+ override fun loadBefore(params: LoadParams<K>, callback: LoadCallback<K, B>) {
+ source.loadBefore(params, object : LoadCallback<K, A>() {
+ override fun onResult(data: List<A>, adjacentPageKey: K?) {
+ callback.onResult(convert(data), adjacentPageKey)
+ }
+ })
+ }
+
+ override fun loadAfter(params: LoadParams<K>, callback: LoadCallback<K, B>) {
+ source.loadAfter(params, object : LoadCallback<K, A>() {
+ override fun onResult(data: List<A>, adjacentPageKey: K?) {
+ callback.onResult(convert(data), adjacentPageKey)
+ }
+ })
+ }
+
+ protected abstract fun convert(source: List<A>): List<B>
+ }
+
+ private class StringWrapperDataSource<K, V>(source: PageKeyedDataSource<K, V>)
+ : WrapperDataSource<K, V, String>(source) {
+ override fun convert(source: List<V>): List<String> {
+ return source.map { it.toString() }
+ }
+ }
+
+ private fun verifyWrappedDataSource(createWrapper:
+ (PageKeyedDataSource<String, Item>) -> PageKeyedDataSource<String, String>) {
+ // verify that it's possible to wrap a PageKeyedDataSource, and add info to its data
+ val orig = ItemDataSource(data = PAGE_MAP)
+ val wrapper = createWrapper(orig)
+
+ // load initial
+ @Suppress("UNCHECKED_CAST")
+ val loadInitialCallback = mock(PageKeyedDataSource.LoadInitialCallback::class.java)
+ as PageKeyedDataSource.LoadInitialCallback<String, String>
+
+ wrapper.loadInitial(PageKeyedDataSource.LoadInitialParams<String>(4, true),
+ loadInitialCallback)
+ val expectedInitial = PAGE_MAP.get(INIT_KEY)!!
+ verify(loadInitialCallback).onResult(expectedInitial.data.map { it.toString() },
+ expectedInitial.prev, expectedInitial.next)
+ verifyNoMoreInteractions(loadInitialCallback)
+
+ @Suppress("UNCHECKED_CAST")
+ val loadCallback = mock(PageKeyedDataSource.LoadCallback::class.java)
+ as PageKeyedDataSource.LoadCallback<String, String>
+ // load after
+ wrapper.loadAfter(PageKeyedDataSource.LoadParams(expectedInitial.next!!, 4), loadCallback)
+ val expectedAfter = PAGE_MAP.get(expectedInitial.next)!!
+ verify(loadCallback).onResult(expectedAfter.data.map { it.toString() },
+ expectedAfter.next)
+ verifyNoMoreInteractions(loadCallback)
+
+ // load before
+ wrapper.loadBefore(PageKeyedDataSource.LoadParams(expectedAfter.prev!!, 4), loadCallback)
+ verify(loadCallback).onResult(expectedInitial.data.map { it.toString() },
+ expectedInitial.prev)
+ verifyNoMoreInteractions(loadCallback)
+ }
+
+ @Test
+ fun testManualWrappedDataSource() = verifyWrappedDataSource {
+ StringWrapperDataSource(it)
+ }
+
+ @Test
+ fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
+ it.mapByPage { it.map { it.toString() } }
+ }
+ @Test
+ fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
+ it.map { it.toString() }
+ }
+
+ companion object {
+ // first load is 2nd page to ensure we test prepend as well as append behavior
+ private val INIT_KEY: String = "key 2"
+ private val PAGE_MAP: Map<String, Page>
+ private val ITEM_LIST: List<Item>
+
+ init {
+ val map = HashMap<String, Page>()
+ val list = ArrayList<Item>()
+ val pageCount = 5
+ for (i in 1..pageCount) {
+ val data = List(4) { Item("name $i $it") }
+ list.addAll(data)
+
+ val key = "key $i"
+ val prev = if (i > 1) ("key " + (i - 1)) else null
+ val next = if (i < pageCount) ("key " + (i + 1)) else null
+ map.put(key, Page(prev, data, next))
+ }
+ PAGE_MAP = map
+ ITEM_LIST = list
+ }
+ }
+
+ private fun drain() {
+ var executed: Boolean
+ do {
+ executed = mBackgroundThread.executeAll()
+ executed = mMainThread.executeAll() || executed
+ } while (executed)
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/PagedStorageTest.kt b/paging/common/src/test/java/androidx/paging/PagedStorageTest.kt
new file mode 100644
index 0000000..4ae9642
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/PagedStorageTest.kt
@@ -0,0 +1,406 @@
+/*
+ * 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 androidx.paging
+
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(JUnit4::class)
+class PagedStorageTest {
+ private fun createPage(vararg strings: String): List<String> {
+ return strings.asList()
+ }
+
+ @Test
+ fun construct() {
+ val storage = PagedStorage(2, createPage("a", "b"), 2)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", null, null), storage.toArray())
+ assertEquals(6, storage.size)
+ }
+
+ @Test
+ fun appendFill() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("a", "b"), 2)
+ storage.appendPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ verify(callback).onPageAppended(4, 2, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun appendAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("a", "b"), 0)
+ storage.appendPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ verify(callback).onPageAppended(4, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun appendFillAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("a", "b"), 2)
+
+ // change 2 nulls into c, d
+ storage.appendPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d"), storage.toArray())
+ verify(callback).onPageAppended(4, 2, 0)
+ verifyNoMoreInteractions(callback)
+
+ // append e, f
+ storage.appendPage(createPage("e", "f"), callback)
+
+ assertArrayEquals(arrayOf(null, null, "a", "b", "c", "d", "e", "f"), storage.toArray())
+ verify(callback).onPageAppended(6, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependFill() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("c", "d"), 2)
+ storage.prependPage(createPage("a", "b"), callback)
+
+ assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 2, 0)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(0, createPage("c", "d"), 2)
+ storage.prependPage(createPage("a", "b"), callback)
+
+ assertArrayEquals(arrayOf("a", "b", "c", "d", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependFillAdd() {
+ val callback = mock(PagedStorage.Callback::class.java)
+
+ val storage = PagedStorage(2, createPage("e", "f"), 2)
+
+ // change 2 nulls into c, d
+ storage.prependPage(createPage("c", "d"), callback)
+
+ assertArrayEquals(arrayOf("c", "d", "e", "f", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 2, 0)
+ verifyNoMoreInteractions(callback)
+
+ // prepend a, b
+ storage.prependPage(createPage("a", "b"), callback)
+
+ assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null, null), storage.toArray())
+ verify(callback).onPagePrepended(0, 0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun isTiled_addend_smallerPageIsNotLast() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a", "a"), 0)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("a", "a"), callback)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("a"), callback)
+ assertTrue(storage.isTiled)
+
+ // no matter what we append here, we're no longer tiled
+ storage.appendPage(createPage("a", "a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun isTiled_append_growingSizeDisable() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a", "a"), 0)
+ assertTrue(storage.isTiled)
+
+ // page size can't grow from append
+ storage.appendPage(createPage("a", "a", "a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun isTiled_prepend_smallerPage() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a"), 0)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a", "a"), callback)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a", "a"), callback)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun isTiled_prepend_smallerThanInitialPage() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(0, createPage("a", "a"), 0)
+ assertTrue(storage.isTiled)
+
+ storage.prependPage(createPage("a"), callback)
+ assertFalse(storage.isTiled)
+ }
+
+ @Test
+ fun get_tiled() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(1, createPage("a", "b"), 5)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("c", "d"), callback)
+ storage.appendPage(createPage("e", "f"), callback)
+
+ assertTrue(storage.isTiled)
+ assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+ }
+
+ @Test
+ fun get_nonTiled() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage(1, createPage("a"), 6)
+ assertTrue(storage.isTiled)
+
+ storage.appendPage(createPage("b", "c"), callback)
+ storage.appendPage(createPage("d", "e", "f"), callback)
+
+ assertFalse(storage.isTiled)
+ assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray())
+ }
+
+ @Test
+ fun insertOne() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+ verify(callback).onInitialized(7)
+ verifyNoMoreInteractions(callback)
+
+ storage.insertPage(4, createPage("e", "f"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, "c", "d", "e", "f", null), storage.toArray())
+ verify(callback).onPageInserted(4, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun insertThree() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+ verify(callback).onInitialized(7)
+ verifyNoMoreInteractions(callback)
+
+ // first, insert 1st page
+ storage.insertPage(0, createPage("a", "b"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", "c", "d", null, null, null), storage.toArray())
+ verify(callback).onPageInserted(0, 2)
+ verifyNoMoreInteractions(callback)
+
+ // then 3rd page
+ storage.insertPage(4, createPage("e", "f"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null), storage.toArray())
+ verify(callback).onPageInserted(4, 2)
+ verifyNoMoreInteractions(callback)
+
+ // then last, small page
+ storage.insertPage(6, createPage("g"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", "g"), storage.toArray())
+ verify(callback).onPageInserted(6, 1)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun insertLastFirst() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(6, createPage("g"), 0, 0, callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf(null, null, null, null, null, null, "g"), storage.toArray())
+ verify(callback).onInitialized(7)
+ verifyNoMoreInteractions(callback)
+
+ // insert 1st page
+ storage.insertPage(0, createPage("a", "b"), callback)
+
+ assertEquals(7, storage.size)
+ assertArrayEquals(arrayOf("a", "b", null, null, null, null, "g"), storage.toArray())
+ verify(callback).onPageInserted(0, 2)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun insertFailure_decreaseLast() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(2, createPage("c", "d"), 0, 0, callback)
+
+ // should throw, page too small
+ storage.insertPage(0, createPage("a"), callback)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun insertFailure_increase() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(0, createPage("a", "b"), 3, 0, callback)
+
+ // should throw, page too big
+ storage.insertPage(2, createPage("c", "d", "e"), callback)
+ }
+
+ @Test
+ fun allocatePlaceholders_simple() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(2, createPage("c"), 2, 0, callback)
+
+ verify(callback).onInitialized(5)
+
+ storage.allocatePlaceholders(2, 1, 1, callback)
+
+ verify(callback).onPagePlaceholderInserted(1)
+ verify(callback).onPagePlaceholderInserted(3)
+ verifyNoMoreInteractions(callback)
+
+ assertArrayEquals(arrayOf(null, null, "c", null, null), storage.toArray())
+ }
+
+ @Test
+ fun allocatePlaceholders_adoptPageSize() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(4, createPage("e"), 0, 0, callback)
+
+ verify(callback).onInitialized(5)
+
+ storage.allocatePlaceholders(0, 2, 2, callback)
+
+ verify(callback).onPagePlaceholderInserted(0)
+ verify(callback).onPagePlaceholderInserted(1)
+ verifyNoMoreInteractions(callback)
+
+ assertArrayEquals(arrayOf(null, null, null, null, "e"), storage.toArray())
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun allocatePlaceholders_cannotShrinkPageSize() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(4, createPage("e", "f"), 0, 0, callback)
+
+ verify(callback).onInitialized(6)
+
+ storage.allocatePlaceholders(0, 2, 1, callback)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun allocatePlaceholders_cannotAdoptPageSize() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(2, createPage("c", "d"), 2, 0, callback)
+
+ verify(callback).onInitialized(6)
+
+ storage.allocatePlaceholders(0, 2, 3, callback)
+ }
+
+ @Test
+ fun get_placeholdersMulti() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(2, createPage("c", "d"), 3, 0, callback)
+
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+
+ storage.allocatePlaceholders(0, 10, 2, callback)
+
+ // allocating placeholders shouldn't affect result of get
+ assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray())
+ }
+
+ @Test
+ fun hasPage() {
+ val callback = mock(PagedStorage.Callback::class.java)
+ val storage = PagedStorage<String>()
+
+ storage.init(4, createPage("e"), 0, 0, callback)
+
+ assertFalse(storage.hasPage(1, 0))
+ assertFalse(storage.hasPage(1, 1))
+ assertFalse(storage.hasPage(1, 2))
+ assertFalse(storage.hasPage(1, 3))
+ assertTrue(storage.hasPage(1, 4))
+
+ assertFalse(storage.hasPage(2, 0))
+ assertFalse(storage.hasPage(2, 1))
+ assertTrue(storage.hasPage(2, 2))
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt
new file mode 100644
index 0000000..acf565b
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/PositionalDataSourceTest.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright 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 androidx.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(JUnit4::class)
+class PositionalDataSourceTest {
+ private fun computeInitialLoadPos(
+ requestedStartPosition: Int,
+ requestedLoadSize: Int,
+ pageSize: Int,
+ totalCount: Int): Int {
+ val params = PositionalDataSource.LoadInitialParams(
+ requestedStartPosition, requestedLoadSize, pageSize, true)
+ return PositionalDataSource.computeInitialLoadPosition(params, totalCount)
+ }
+
+ @Test
+ fun computeInitialLoadPositionZero() {
+ assertEquals(0, computeInitialLoadPos(
+ requestedStartPosition = 0,
+ requestedLoadSize = 30,
+ pageSize = 10,
+ totalCount = 100))
+ }
+
+ @Test
+ fun computeInitialLoadPositionRequestedPositionIncluded() {
+ assertEquals(10, computeInitialLoadPos(
+ requestedStartPosition = 10,
+ requestedLoadSize = 10,
+ pageSize = 10,
+ totalCount = 100))
+ }
+
+ @Test
+ fun computeInitialLoadPositionRound() {
+ assertEquals(10, computeInitialLoadPos(
+ requestedStartPosition = 13,
+ requestedLoadSize = 30,
+ pageSize = 10,
+ totalCount = 100))
+ }
+
+ @Test
+ fun computeInitialLoadPositionEndAdjusted() {
+ assertEquals(70, computeInitialLoadPos(
+ requestedStartPosition = 99,
+ requestedLoadSize = 30,
+ pageSize = 10,
+ totalCount = 100))
+ }
+
+ @Test
+ fun computeInitialLoadPositionEndAdjustedAndAligned() {
+ assertEquals(70, computeInitialLoadPos(
+ requestedStartPosition = 99,
+ requestedLoadSize = 35,
+ pageSize = 10,
+ totalCount = 100))
+ }
+
+ @Test
+ fun fullLoadWrappedAsContiguous() {
+ // verify that prepend / append work correctly with a PositionalDataSource, made contiguous
+ val config = PagedList.Config.Builder()
+ .setPageSize(10)
+ .setInitialLoadSizeHint(10)
+ .setEnablePlaceholders(true)
+ .build()
+ val dataSource: PositionalDataSource<Int> = ListDataSource((0..99).toList())
+ val testExecutor = TestExecutor()
+ val pagedList = ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
+ testExecutor, testExecutor, null, config, 15,
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+
+ assertEquals((10..19).toList(), pagedList)
+
+ // prepend + append work correctly
+ pagedList.loadAround(5)
+ testExecutor.executeAll()
+ assertEquals((0..29).toList(), pagedList)
+
+ // and load the rest of the data to be sure further appends work
+ for (i in (2..9)) {
+ pagedList.loadAround(i * 10 - 5)
+ testExecutor.executeAll()
+ assertEquals((0..i * 10 + 9).toList(), pagedList)
+ }
+ }
+
+ private fun performLoadInitial(
+ enablePlaceholders: Boolean = true,
+ invalidateDataSource: Boolean = false,
+ callbackInvoker: (callback: PositionalDataSource.LoadInitialCallback<String>) -> Unit) {
+ val dataSource = object : PositionalDataSource<String>() {
+ override fun loadInitial(
+ params: LoadInitialParams,
+ callback: LoadInitialCallback<String>) {
+ if (invalidateDataSource) {
+ // invalidate data source so it's invalid when onResult() called
+ invalidate()
+ }
+ callbackInvoker(callback)
+ }
+
+ override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<String>) {
+ fail("loadRange not expected")
+ }
+ }
+
+ val config = PagedList.Config.Builder()
+ .setPageSize(10)
+ .setEnablePlaceholders(enablePlaceholders)
+ .build()
+ if (enablePlaceholders) {
+ TiledPagedList(dataSource, FailExecutor(), FailExecutor(), null, config, 0)
+ } else {
+ ContiguousPagedList(dataSource.wrapAsContiguousWithoutPlaceholders(),
+ FailExecutor(), FailExecutor(), null, config, null,
+ ContiguousPagedList.LAST_LOAD_UNSPECIFIED)
+ }
+ }
+
+ @Test
+ fun initialLoadCallbackSuccess() = performLoadInitial {
+ // LoadInitialCallback correct usage
+ it.onResult(listOf("a", "b"), 0, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackNotPageSizeMultiple() = performLoadInitial {
+ // Positional LoadInitialCallback can't accept result that's not a multiple of page size
+ val elevenLetterList = List(11) { "" + 'a' + it }
+ it.onResult(elevenLetterList, 0, 12)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackListTooBig() = performLoadInitial {
+ // LoadInitialCallback can't accept pos + list > totalCount
+ it.onResult(listOf("a", "b", "c"), 0, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackPositionTooLarge() = performLoadInitial {
+ // LoadInitialCallback can't accept pos + list > totalCount
+ it.onResult(listOf("a", "b"), 1, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackPositionNegative() = performLoadInitial {
+ // LoadInitialCallback can't accept negative position
+ it.onResult(listOf("a", "b", "c"), -1, 2)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackEmptyCannotHavePlaceholders() = performLoadInitial {
+ // LoadInitialCallback can't accept empty result unless data set is empty
+ it.onResult(emptyList(), 0, 2)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun initialLoadCallbackRequireTotalCount() = performLoadInitial(enablePlaceholders = true) {
+ // LoadInitialCallback requires 3 args when placeholders enabled
+ it.onResult(listOf("a", "b"), 0)
+ }
+
+ @Test
+ fun initialLoadCallbackSuccessTwoArg() = performLoadInitial(enablePlaceholders = false) {
+ // LoadInitialCallback correct 2 arg usage
+ it.onResult(listOf("a", "b"), 0)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackPosNegativeTwoArg() = performLoadInitial(enablePlaceholders = false) {
+ // LoadInitialCallback can't accept negative position
+ it.onResult(listOf("a", "b"), -1)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun initialLoadCallbackEmptyWithOffset() = performLoadInitial(enablePlaceholders = false) {
+ // LoadInitialCallback can't accept empty result unless pos is 0
+ it.onResult(emptyList(), 1)
+ }
+
+ @Test
+ fun initialLoadCallbackInvalidTwoArg() = performLoadInitial(invalidateDataSource = true) {
+ // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
+ it.onResult(emptyList(), 1)
+ }
+
+ @Test
+ fun initialLoadCallbackInvalidThreeArg() = performLoadInitial(invalidateDataSource = true) {
+ // LoadInitialCallback doesn't throw on invalid args if DataSource is invalid
+ it.onResult(emptyList(), 0, 1)
+ }
+
+ private abstract class WrapperDataSource<in A, B>(private val source: PositionalDataSource<A>)
+ : PositionalDataSource<B>() {
+ private val invalidatedCallback = DataSource.InvalidatedCallback {
+ invalidate()
+ removeCallback()
+ }
+
+ init {
+ source.addInvalidatedCallback(invalidatedCallback)
+ }
+
+ private fun removeCallback() {
+ removeInvalidatedCallback(invalidatedCallback)
+ }
+
+ override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<B>) {
+ source.loadInitial(params, object : LoadInitialCallback<A>() {
+ override fun onResult(data: List<A>, position: Int, totalCount: Int) {
+ callback.onResult(convert(data), position, totalCount)
+ }
+
+ override fun onResult(data: List<A>, position: Int) {
+ callback.onResult(convert(data), position)
+ }
+ })
+ }
+
+ override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<B>) {
+ source.loadRange(params, object : LoadRangeCallback<A>() {
+ override fun onResult(data: List<A>) {
+ callback.onResult(convert(data))
+ }
+ })
+ }
+
+ protected abstract fun convert(source: List<A>): List<B>
+ }
+
+ private class StringWrapperDataSource<in A>(source: PositionalDataSource<A>)
+ : WrapperDataSource<A, String>(source) {
+ override fun convert(source: List<A>): List<String> {
+ return source.map { it.toString() }
+ }
+ }
+
+ private fun verifyWrappedDataSource(
+ createWrapper: (PositionalDataSource<Int>) -> PositionalDataSource<String>) {
+ val orig = ListDataSource(listOf(0, 5, 4, 8, 12))
+ val wrapper = createWrapper(orig)
+
+ // load initial
+ @Suppress("UNCHECKED_CAST")
+ val loadInitialCallback = mock(PositionalDataSource.LoadInitialCallback::class.java)
+ as PositionalDataSource.LoadInitialCallback<String>
+
+ wrapper.loadInitial(PositionalDataSource.LoadInitialParams(0, 2, 1, true),
+ loadInitialCallback)
+ verify(loadInitialCallback).onResult(listOf("0", "5"), 0, 5)
+ verifyNoMoreInteractions(loadInitialCallback)
+
+ // load range
+ @Suppress("UNCHECKED_CAST")
+ val loadRangeCallback = mock(PositionalDataSource.LoadRangeCallback::class.java)
+ as PositionalDataSource.LoadRangeCallback<String>
+
+ wrapper.loadRange(PositionalDataSource.LoadRangeParams(2, 3), loadRangeCallback)
+ verify(loadRangeCallback).onResult(listOf("4", "8", "12"))
+ verifyNoMoreInteractions(loadRangeCallback)
+
+ // check invalidation behavior
+ val invalCallback = mock(DataSource.InvalidatedCallback::class.java)
+ wrapper.addInvalidatedCallback(invalCallback)
+ orig.invalidate()
+ verify(invalCallback).onInvalidated()
+ verifyNoMoreInteractions(invalCallback)
+ }
+
+ @Test
+ fun testManualWrappedDataSource() = verifyWrappedDataSource {
+ StringWrapperDataSource(it)
+ }
+
+ @Test
+ fun testListConverterWrappedDataSource() = verifyWrappedDataSource {
+ it.mapByPage { it.map { it.toString() } }
+ }
+
+ @Test
+ fun testItemConverterWrappedDataSource() = verifyWrappedDataSource {
+ it.map { it.toString() }
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/TiledDataSourceTest.kt b/paging/common/src/test/java/androidx/paging/TiledDataSourceTest.kt
new file mode 100644
index 0000000..5067e46
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/TiledDataSourceTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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 androidx.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.Collections
+
+@Suppress("DEPRECATION")
+@RunWith(JUnit4::class)
+class TiledDataSourceTest {
+
+ fun TiledDataSource<String>.loadInitial(
+ startPosition: Int, count: Int, pageSize: Int): List<String> {
+ @Suppress("UNCHECKED_CAST")
+ val receiver = mock(PageResult.Receiver::class.java) as PageResult.Receiver<String>
+
+ this.dispatchLoadInitial(true, startPosition, count, pageSize, FailExecutor(), receiver)
+
+ @Suppress("UNCHECKED_CAST")
+ val argument = ArgumentCaptor.forClass(PageResult::class.java)
+ as ArgumentCaptor<PageResult<String>>
+ verify(receiver).onPageResult(eq(PageResult.INIT), argument.capture())
+ verifyNoMoreInteractions(receiver)
+
+ val observed = argument.value
+
+ return observed.page
+ }
+
+ @Test
+ fun loadInitialEmpty() {
+ class EmptyDataSource : TiledDataSource<String>() {
+ override fun countItems(): Int {
+ return 0
+ }
+
+ override fun loadRange(startPosition: Int, count: Int): List<String> {
+ return emptyList()
+ }
+ }
+
+ assertEquals(Collections.EMPTY_LIST, EmptyDataSource().loadInitial(0, 1, 5))
+ }
+
+ @Test
+ fun loadInitialTooLong() {
+ val list = List(26) { "" + 'a' + it }
+ class AlphabetDataSource : TiledDataSource<String>() {
+ override fun countItems(): Int {
+ return list.size
+ }
+
+ override fun loadRange(startPosition: Int, count: Int): List<String> {
+ return list.subList(startPosition, startPosition + count)
+ }
+ }
+ // baseline behavior
+ assertEquals(list, AlphabetDataSource().loadInitial(0, 26, 10))
+ assertEquals(list, AlphabetDataSource().loadInitial(50, 26, 10))
+ }
+}
diff --git a/paging/common/src/test/java/androidx/paging/TiledPagedListTest.kt b/paging/common/src/test/java/androidx/paging/TiledPagedListTest.kt
new file mode 100644
index 0000000..2baf0aa
--- /dev/null
+++ b/paging/common/src/test/java/androidx/paging/TiledPagedListTest.kt
@@ -0,0 +1,468 @@
+/*
+ * 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 androidx.paging
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+
+@RunWith(JUnit4::class)
+class TiledPagedListTest {
+ private val mMainThread = TestExecutor()
+ private val mBackgroundThread = TestExecutor()
+
+ private class Item(position: Int) {
+ val name: String = "Item $position"
+
+ override fun toString(): String {
+ return name
+ }
+ }
+
+ private fun verifyLoadedPages(list: List<Item>, vararg loadedPages: Int) {
+ val loadedPageList = loadedPages.asList()
+ assertEquals(ITEMS.size, list.size)
+ for (i in list.indices) {
+ if (loadedPageList.contains(i / PAGE_SIZE)) {
+ assertSame("Index $i", ITEMS[i], list[i])
+ } else {
+ assertNull("Index $i", list[i])
+ }
+ }
+ }
+
+ private fun createTiledPagedList(loadPosition: Int, initPageCount: Int,
+ prefetchDistance: Int = PAGE_SIZE,
+ listData: List<Item> = ITEMS,
+ boundaryCallback: PagedList.BoundaryCallback<Item>? = null): TiledPagedList<Item> {
+ return TiledPagedList(
+ ListDataSource(listData), mMainThread, mBackgroundThread, boundaryCallback,
+ PagedList.Config.Builder()
+ .setPageSize(PAGE_SIZE)
+ .setInitialLoadSizeHint(PAGE_SIZE * initPageCount)
+ .setPrefetchDistance(prefetchDistance)
+ .build(),
+ loadPosition)
+ }
+
+ @Test
+ fun getDataSource() {
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
+ assertTrue(pagedList.dataSource is ListDataSource<Item>)
+
+ // snapshot keeps same DataSource
+ assertSame(pagedList.dataSource,
+ (pagedList.snapshot() as SnapshotPagedList<Item>).dataSource)
+ }
+
+ @Test
+ fun initialLoad_onePage() {
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
+ verifyLoadedPages(pagedList, 0, 1)
+ }
+
+ @Test
+ fun initialLoad_onePageOffset() {
+ val pagedList = createTiledPagedList(loadPosition = 10, initPageCount = 1)
+ verifyLoadedPages(pagedList, 0, 1)
+ }
+
+ @Test
+ fun initialLoad_full() {
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 100)
+ verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+ }
+
+ @Test
+ fun initialLoad_end() {
+ val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
+ verifyLoadedPages(pagedList, 3, 4)
+ }
+
+ @Test
+ fun initialLoad_multiple() {
+ val pagedList = createTiledPagedList(loadPosition = 9, initPageCount = 2)
+ verifyLoadedPages(pagedList, 0, 1)
+ }
+
+ @Test
+ fun initialLoad_offset() {
+ val pagedList = createTiledPagedList(loadPosition = 41, initPageCount = 2)
+ verifyLoadedPages(pagedList, 3, 4)
+ }
+
+ @Test
+ fun initialLoad_initializesLastKey() {
+ val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
+ assertEquals(44, pagedList.lastKey)
+ }
+
+ @Test
+ fun initialLoadAsync() {
+ val dataSource = AsyncListDataSource(ITEMS)
+ val pagedList = TiledPagedList(
+ dataSource, mMainThread, mBackgroundThread, null,
+ PagedList.Config.Builder().setPageSize(10).build(), 0)
+
+ assertTrue(pagedList.isEmpty())
+ drain()
+ assertTrue(pagedList.isEmpty())
+ dataSource.flush()
+ assertTrue(pagedList.isEmpty())
+ mBackgroundThread.executeAll()
+ assertTrue(pagedList.isEmpty())
+
+ // Data source defers callbacks until flush, which posts result to main thread
+ mMainThread.executeAll()
+ assertFalse(pagedList.isEmpty())
+ }
+
+ @Test
+ fun addWeakCallbackEmpty() {
+ val dataSource = AsyncListDataSource(ITEMS)
+ val pagedList = TiledPagedList(
+ dataSource, mMainThread, mBackgroundThread, null,
+ PagedList.Config.Builder().setPageSize(10).build(), 0)
+
+ // capture empty snapshot
+ val emptySnapshot = pagedList.snapshot()
+ assertTrue(pagedList.isEmpty())
+ assertTrue(emptySnapshot.isEmpty())
+
+ // data added in asynchronously
+ dataSource.flush()
+ drain()
+ assertFalse(pagedList.isEmpty())
+
+ // verify that adding callback works with empty start point
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(emptySnapshot, callback)
+ verify(callback).onInserted(0, pagedList.size)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun append() {
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyLoadedPages(pagedList, 0, 1)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(15)
+
+ verifyLoadedPages(pagedList, 0, 1)
+
+ drain()
+
+ verifyLoadedPages(pagedList, 0, 1, 2)
+ verify(callback).onChanged(20, 10)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prepend() {
+ val pagedList = createTiledPagedList(loadPosition = 44, initPageCount = 2)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyLoadedPages(pagedList, 3, 4)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(35)
+ drain()
+
+ verifyLoadedPages(pagedList, 2, 3, 4)
+ verify<PagedList.Callback>(callback).onChanged(20, 10)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun loadWithGap() {
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyLoadedPages(pagedList, 0, 1)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(44)
+ drain()
+
+ verifyLoadedPages(pagedList, 0, 1, 3, 4)
+ verify(callback).onChanged(30, 10)
+ verify(callback).onChanged(40, 5)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun tinyPrefetchTest() {
+ val pagedList = createTiledPagedList(
+ loadPosition = 0, initPageCount = 1, prefetchDistance = 1)
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(null, callback)
+ verifyLoadedPages(pagedList, 0, 1)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(33)
+ drain()
+
+ verifyLoadedPages(pagedList, 0, 1, 3)
+ verify(callback).onChanged(30, 10)
+ verifyNoMoreInteractions(callback)
+
+ pagedList.loadAround(44)
+ drain()
+
+ verifyLoadedPages(pagedList, 0, 1, 3, 4)
+ verify(callback).onChanged(40, 5)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun appendCallbackAddedLate() {
+ val pagedList = createTiledPagedList(
+ loadPosition = 0, initPageCount = 1, prefetchDistance = 0)
+ verifyLoadedPages(pagedList, 0, 1)
+
+ pagedList.loadAround(25)
+ drain()
+ verifyLoadedPages(pagedList, 0, 1, 2)
+
+ // snapshot at 30 items
+ val snapshot = pagedList.snapshot()
+ verifyLoadedPages(snapshot, 0, 1, 2)
+
+ pagedList.loadAround(35)
+ pagedList.loadAround(44)
+ drain()
+ verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+ verifyLoadedPages(snapshot, 0, 1, 2)
+
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(snapshot, callback)
+ verify(callback).onChanged(30, 20)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun prependCallbackAddedLate() {
+ val pagedList = createTiledPagedList(
+ loadPosition = 44, initPageCount = 2, prefetchDistance = 0)
+ verifyLoadedPages(pagedList, 3, 4)
+
+ pagedList.loadAround(25)
+ drain()
+ verifyLoadedPages(pagedList, 2, 3, 4)
+
+ // snapshot at 30 items
+ val snapshot = pagedList.snapshot()
+ verifyLoadedPages(snapshot, 2, 3, 4)
+
+ pagedList.loadAround(15)
+ pagedList.loadAround(5)
+ drain()
+ verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+ verifyLoadedPages(snapshot, 2, 3, 4)
+
+ val callback = mock(PagedList.Callback::class.java)
+ pagedList.addWeakCallback(snapshot, callback)
+ verify(callback).onChanged(0, 20)
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun placeholdersDisabled() {
+ // disable placeholders with config, so we create a contiguous version of the pagedlist
+ val config = PagedList.Config.Builder()
+ .setPageSize(PAGE_SIZE)
+ .setPrefetchDistance(PAGE_SIZE)
+ .setInitialLoadSizeHint(PAGE_SIZE)
+ .setEnablePlaceholders(false)
+ .build()
+ val pagedList = PagedList.Builder<Int, Item>(ListDataSource(ITEMS), config)
+ .setMainThreadExecutor(mMainThread)
+ .setBackgroundThreadExecutor(mBackgroundThread)
+ .setInitialKey(20)
+ .build()
+
+ assertTrue(pagedList.isContiguous)
+
+ @Suppress("UNCHECKED_CAST")
+ val contiguousPagedList = pagedList as ContiguousPagedList<Int, Item>
+ assertEquals(0, contiguousPagedList.mStorage.leadingNullCount)
+ assertEquals(PAGE_SIZE, contiguousPagedList.mStorage.storageCount)
+ assertEquals(0, contiguousPagedList.mStorage.trailingNullCount)
+ }
+
+ @Test
+ fun boundaryCallback_empty() {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
+ listData = ArrayList(), boundaryCallback = boundaryCallback)
+ assertEquals(0, pagedList.size)
+
+ // nothing yet
+ verifyNoMoreInteractions(boundaryCallback)
+
+ // onZeroItemsLoaded posted, since creation often happens on BG thread
+ drain()
+ verify(boundaryCallback).onZeroItemsLoaded()
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun boundaryCallback_immediate() {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 1,
+ listData = ITEMS.subList(0, 2), boundaryCallback = boundaryCallback)
+ assertEquals(2, pagedList.size)
+
+ // nothing yet
+ verifyZeroInteractions(boundaryCallback)
+
+ // callbacks posted, since creation often happens on BG thread
+ drain()
+ verify(boundaryCallback).onItemAtFrontLoaded(ITEMS[0])
+ verify(boundaryCallback).onItemAtEndLoaded(ITEMS[1])
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun boundaryCallback_delayedUntilLoaded() {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createTiledPagedList(loadPosition = 20, initPageCount = 1,
+ boundaryCallback = boundaryCallback)
+ verifyLoadedPages(pagedList, 1, 2) // 0, 3, and 4 not loaded yet
+
+ // nothing yet, even after drain
+ verifyZeroInteractions(boundaryCallback)
+ drain()
+ verifyZeroInteractions(boundaryCallback)
+
+ pagedList.loadAround(0)
+ pagedList.loadAround(44)
+
+ // still nothing, since items aren't loaded...
+ verifyZeroInteractions(boundaryCallback)
+
+ drain()
+ // first/last items loaded now, so callbacks dispatched
+ verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
+ verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun boundaryCallback_delayedUntilNearbyAccess() {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val pagedList = createTiledPagedList(loadPosition = 0, initPageCount = 5,
+ prefetchDistance = 2, boundaryCallback = boundaryCallback)
+ verifyLoadedPages(pagedList, 0, 1, 2, 3, 4)
+
+ // all items loaded, but no access near ends, so no callbacks
+ verifyZeroInteractions(boundaryCallback)
+ drain()
+ verifyZeroInteractions(boundaryCallback)
+
+ pagedList.loadAround(0)
+ pagedList.loadAround(44)
+
+ // callbacks not posted immediately
+ verifyZeroInteractions(boundaryCallback)
+
+ drain()
+
+ // items accessed, so now posted callbacks are run
+ verify(boundaryCallback).onItemAtFrontLoaded(ITEMS.first())
+ verify(boundaryCallback).onItemAtEndLoaded(ITEMS.last())
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ private fun validateCallbackForSize(initPageCount: Int, itemCount: Int) {
+ @Suppress("UNCHECKED_CAST")
+ val boundaryCallback =
+ mock(PagedList.BoundaryCallback::class.java) as PagedList.BoundaryCallback<Item>
+ val listData = ITEMS.subList(0, itemCount)
+ val pagedList = createTiledPagedList(
+ loadPosition = 0,
+ initPageCount = initPageCount,
+ prefetchDistance = 0,
+ boundaryCallback = boundaryCallback,
+ listData = listData)
+ assertNotNull(pagedList[pagedList.size - 1 - PAGE_SIZE])
+ assertNull(pagedList.last()) // not completed loading
+
+ // no access near list beginning, so no callbacks yet
+ verifyNoMoreInteractions(boundaryCallback)
+ drain()
+ verifyNoMoreInteractions(boundaryCallback)
+
+ // trigger front boundary callback (via access)
+ pagedList.loadAround(0)
+ drain()
+ verify(boundaryCallback).onItemAtFrontLoaded(listData.first())
+ verifyNoMoreInteractions(boundaryCallback)
+
+ // trigger end boundary callback (via load)
+ pagedList.loadAround(pagedList.size - 1)
+ drain()
+ verify(boundaryCallback).onItemAtEndLoaded(listData.last())
+ verifyNoMoreInteractions(boundaryCallback)
+ }
+
+ @Test
+ fun boundaryCallbackPageSize1() {
+ // verify different alignments of last page still trigger boundaryCallback correctly
+ validateCallbackForSize(2, 3 * PAGE_SIZE - 2)
+ validateCallbackForSize(2, 3 * PAGE_SIZE - 1)
+ validateCallbackForSize(2, 3 * PAGE_SIZE)
+ validateCallbackForSize(3, 3 * PAGE_SIZE + 1)
+ validateCallbackForSize(3, 3 * PAGE_SIZE + 2)
+ }
+
+ private fun drain() {
+ var executed: Boolean
+ do {
+ executed = mBackgroundThread.executeAll()
+ executed = mMainThread.executeAll() || executed
+ } while (executed)
+ }
+
+ companion object {
+ // use a page size that's not an even divisor of ITEMS.size() to test end conditions
+ private val PAGE_SIZE = 10
+
+ private val ITEMS = List(45) { Item(it) }
+ }
+}
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index 9aab333..4264aef 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -22,16 +22,16 @@
}
dependencies {
- implementation(project(":arch:runtime"))
- implementation(project(":arch:common"))
- implementation(project(":paging:common"))
- implementation(project(":lifecycle:extensions"))
- implementation(project(":lifecycle:runtime"))
- implementation(project(":lifecycle:common"))
- implementation(project(":paging:runtime"))
+ implementation(project(":arch:core-runtime"))
+ implementation(project(":arch:core-common"))
+ implementation(project(":paging:paging-common"))
+ implementation(project(":lifecycle:lifecycle-extensions"))
+ implementation(project(":lifecycle:lifecycle-runtime"))
+ implementation(project(":lifecycle:lifecycle-common"))
+ implementation(project(":paging:paging-runtime"))
implementation(MULTIDEX)
- implementation(SUPPORT_RECYCLERVIEW_27, libs.support_exclude_config)
- implementation(SUPPORT_APPCOMPAT_27, libs.support_exclude_config)
+ implementation(SUPPORT_RECYCLERVIEW, libs.support_exclude_config)
+ implementation(SUPPORT_APPCOMPAT, libs.support_exclude_config)
}
tasks['check'].dependsOn(tasks['connectedCheck'])
diff --git a/paging/integration-tests/testapp/src/main/AndroidManifest.xml b/paging/integration-tests/testapp/src/main/AndroidManifest.xml
index 8ddf651..168c16f 100644
--- a/paging/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/paging/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -15,13 +15,13 @@
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.paging.integration.testapp">
+ package="androidx.paging.integration.testapp">
<application
android:allowBackup="true"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat">
<activity
- android:name=".PagedListSampleActivity"
+ android:name="androidx.paging.integration.testapp.PagedListSampleActivity"
android:label="PagedList Sample">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/Item.java b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/Item.java
deleted file mode 100644
index 94d25a8..0000000
--- a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/Item.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.paging.integration.testapp;
-
-import android.support.annotation.NonNull;
-import android.support.v7.util.DiffUtil;
-
-/**
- * Sample item.
- */
-class Item {
- public final int id;
- public final String text;
- @SuppressWarnings("WeakerAccess")
- public final int bgColor;
-
- Item(int id, String text, int bgColor) {
- this.id = id;
- this.text = text;
- this.bgColor = bgColor;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Item item = (Item) o;
- return this.id == item.id
- && this.bgColor == item.bgColor
- && this.text.equals(item.text);
- }
-
- static final DiffUtil.ItemCallback<Item> DIFF_CALLBACK = new DiffUtil.ItemCallback<Item>() {
- @Override
- public boolean areContentsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
- return oldItem.equals(newItem);
- }
-
- @Override
- public boolean areItemsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
- return oldItem.id == newItem.id;
- }
- };
-}
diff --git a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/ItemDataSource.java b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/ItemDataSource.java
deleted file mode 100644
index c53d361..0000000
--- a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/ItemDataSource.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.paging.integration.testapp;
-
-import android.arch.paging.PositionalDataSource;
-import android.graphics.Color;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Sample data source with artificial data.
- */
-class ItemDataSource extends PositionalDataSource<Item> {
- private static final int COUNT = 500;
-
- @ColorInt
- private static final int[] COLORS = new int[] {
- Color.RED,
- Color.BLUE,
- Color.BLACK,
- };
-
- private static int sGenerationId;
- private final int mGenerationId = sGenerationId++;
-
- private List<Item> loadRangeInternal(int startPosition, int loadCount) {
- List<Item> items = new ArrayList<>();
- int end = Math.min(COUNT, startPosition + loadCount);
- int bgColor = COLORS[mGenerationId % COLORS.length];
-
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- for (int i = startPosition; i != end; i++) {
- items.add(new Item(i, "item " + i, bgColor));
- }
- return items;
- }
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams params,
- @NonNull LoadInitialCallback<Item> callback) {
- int position = computeInitialLoadPosition(params, COUNT);
- int loadSize = computeInitialLoadSize(params, position, COUNT);
- List<Item> data = loadRangeInternal(position, loadSize);
- callback.onResult(data, position, COUNT);
- }
-
- @Override
- public void loadRange(@NonNull LoadRangeParams params,
- @NonNull LoadRangeCallback<Item> callback) {
- List<Item> data = loadRangeInternal(params.startPosition, params.loadSize);
- callback.onResult(data);
- }
-}
diff --git a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemAdapter.java b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemAdapter.java
deleted file mode 100644
index 9432124..0000000
--- a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemAdapter.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.paging.integration.testapp;
-
-import android.arch.paging.PagedListAdapter;
-import android.graphics.Color;
-import android.support.v7.widget.RecyclerView;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-/**
- * Sample PagedList item Adapter, which uses a AsyncPagedListDiffer.
- */
-class PagedListItemAdapter extends PagedListAdapter<Item, RecyclerView.ViewHolder> {
-
- PagedListItemAdapter() {
- super(Item.DIFF_CALLBACK);
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(
- new TextView(parent.getContext())) {};
- holder.itemView.setMinimumHeight(400);
- return holder;
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
- Item item = getItem(position);
- if (item != null) {
- ((TextView) (holder.itemView)).setText(item.text);
- holder.itemView.setBackgroundColor(item.bgColor);
- } else {
- ((TextView) (holder.itemView)).setText(R.string.loading);
- holder.itemView.setBackgroundColor(Color.TRANSPARENT);
- }
- }
-}
diff --git a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java
deleted file mode 100644
index be14bc1..0000000
--- a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListItemViewModel.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.paging.integration.testapp;
-
-import android.arch.lifecycle.LiveData;
-import android.arch.lifecycle.ViewModel;
-import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListBuilder;
-import android.arch.paging.PagedList;
-
-/**
- * Sample ViewModel backed by an artificial data source
- */
-@SuppressWarnings("WeakerAccess")
-public class PagedListItemViewModel extends ViewModel {
- private ItemDataSource mDataSource;
- private final Object mDataSourceLock = new Object();
-
- private final DataSource.Factory<Integer, Item> mFactory =
- new DataSource.Factory<Integer, Item>() {
- @Override
- public DataSource<Integer, Item> create() {
- ItemDataSource newDataSource = new ItemDataSource();
- synchronized (mDataSourceLock) {
- mDataSource = newDataSource;
- return mDataSource;
- }
- }
- };
-
- private LiveData<PagedList<Item>> mLivePagedList =
- new LivePagedListBuilder<>(mFactory, 20).build();
-
- void invalidateList() {
- synchronized (mDataSourceLock) {
- if (mDataSource != null) {
- mDataSource.invalidate();
- }
- }
- }
-
- LiveData<PagedList<Item>> getLivePagedList() {
- return mLivePagedList;
- }
-}
diff --git a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListSampleActivity.java b/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListSampleActivity.java
deleted file mode 100644
index f031cdb..0000000
--- a/paging/integration-tests/testapp/src/main/java/android/arch/paging/integration/testapp/PagedListSampleActivity.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.paging.integration.testapp;
-
-import android.arch.lifecycle.Observer;
-import android.arch.lifecycle.ViewModelProviders;
-import android.arch.paging.PagedList;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import android.widget.Button;
-
-/**
- * Sample PagedList activity with artificial data source.
- */
-public class PagedListSampleActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_recycler_view);
- final PagedListItemViewModel viewModel = ViewModelProviders.of(this)
- .get(PagedListItemViewModel.class);
-
- final PagedListItemAdapter adapter = new PagedListItemAdapter();
- final RecyclerView recyclerView = findViewById(R.id.recyclerview);
- recyclerView.setAdapter(adapter);
- viewModel.getLivePagedList().observe(this, new Observer<PagedList<Item>>() {
- @Override
- public void onChanged(@Nullable PagedList<Item> items) {
- adapter.submitList(items);
- }
- });
- final Button button = findViewById(R.id.button);
- button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- viewModel.invalidateList();
- }
- });
- }
-}
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/Item.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/Item.java
new file mode 100644
index 0000000..17cb348
--- /dev/null
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/Item.java
@@ -0,0 +1,59 @@
+/*
+ * 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 androidx.paging.integration.testapp;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+
+/**
+ * Sample item.
+ */
+class Item {
+ public final int id;
+ public final String text;
+ @SuppressWarnings("WeakerAccess")
+ public final int bgColor;
+
+ Item(int id, String text, int bgColor) {
+ this.id = id;
+ this.text = text;
+ this.bgColor = bgColor;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Item item = (Item) o;
+ return this.id == item.id
+ && this.bgColor == item.bgColor
+ && this.text.equals(item.text);
+ }
+
+ static final DiffUtil.ItemCallback<Item> DIFF_CALLBACK = new DiffUtil.ItemCallback<Item>() {
+ @Override
+ public boolean areContentsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
+ return oldItem.equals(newItem);
+ }
+
+ @Override
+ public boolean areItemsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
+ return oldItem.id == newItem.id;
+ }
+ };
+}
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/ItemDataSource.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/ItemDataSource.java
new file mode 100644
index 0000000..064b05c
--- /dev/null
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/ItemDataSource.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 androidx.paging.integration.testapp;
+
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.paging.PositionalDataSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sample data source with artificial data.
+ */
+class ItemDataSource extends PositionalDataSource<Item> {
+ private static final int COUNT = 500;
+
+ @ColorInt
+ private static final int[] COLORS = new int[] {
+ Color.RED,
+ Color.BLUE,
+ Color.BLACK,
+ };
+
+ private static int sGenerationId;
+ private final int mGenerationId = sGenerationId++;
+
+ private List<Item> loadRangeInternal(int startPosition, int loadCount) {
+ List<Item> items = new ArrayList<>();
+ int end = Math.min(COUNT, startPosition + loadCount);
+ int bgColor = COLORS[mGenerationId % COLORS.length];
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ for (int i = startPosition; i != end; i++) {
+ items.add(new Item(i, "item " + i, bgColor));
+ }
+ return items;
+ }
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<Item> callback) {
+ int position = computeInitialLoadPosition(params, COUNT);
+ int loadSize = computeInitialLoadSize(params, position, COUNT);
+ List<Item> data = loadRangeInternal(position, loadSize);
+ callback.onResult(data, position, COUNT);
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<Item> callback) {
+ List<Item> data = loadRangeInternal(params.startPosition, params.loadSize);
+ callback.onResult(data);
+ }
+}
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListItemAdapter.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListItemAdapter.java
new file mode 100644
index 0000000..0baa80c
--- /dev/null
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListItemAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * 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 androidx.paging.integration.testapp;
+
+import android.graphics.Color;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.paging.PagedListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Sample PagedList item Adapter, which uses a AsyncPagedListDiffer.
+ */
+class PagedListItemAdapter extends PagedListAdapter<Item, RecyclerView.ViewHolder> {
+
+ PagedListItemAdapter() {
+ super(Item.DIFF_CALLBACK);
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(
+ new TextView(parent.getContext())) {};
+ holder.itemView.setMinimumHeight(400);
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ Item item = getItem(position);
+ if (item != null) {
+ ((TextView) (holder.itemView)).setText(item.text);
+ holder.itemView.setBackgroundColor(item.bgColor);
+ } else {
+ ((TextView) (holder.itemView)).setText(R.string.loading);
+ holder.itemView.setBackgroundColor(Color.TRANSPARENT);
+ }
+ }
+}
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListItemViewModel.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListItemViewModel.java
new file mode 100644
index 0000000..08f3af2
--- /dev/null
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListItemViewModel.java
@@ -0,0 +1,59 @@
+/*
+ * 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 androidx.paging.integration.testapp;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.paging.DataSource;
+import androidx.paging.LivePagedListBuilder;
+import androidx.paging.PagedList;
+
+/**
+ * Sample ViewModel backed by an artificial data source
+ */
+@SuppressWarnings("WeakerAccess")
+public class PagedListItemViewModel extends ViewModel {
+ private ItemDataSource mDataSource;
+ private final Object mDataSourceLock = new Object();
+
+ private final DataSource.Factory<Integer, Item> mFactory =
+ new DataSource.Factory<Integer, Item>() {
+ @Override
+ public DataSource<Integer, Item> create() {
+ ItemDataSource newDataSource = new ItemDataSource();
+ synchronized (mDataSourceLock) {
+ mDataSource = newDataSource;
+ return mDataSource;
+ }
+ }
+ };
+
+ private LiveData<PagedList<Item>> mLivePagedList =
+ new LivePagedListBuilder<>(mFactory, 20).build();
+
+ void invalidateList() {
+ synchronized (mDataSourceLock) {
+ if (mDataSource != null) {
+ mDataSource.invalidate();
+ }
+ }
+ }
+
+ LiveData<PagedList<Item>> getLivePagedList() {
+ return mLivePagedList;
+ }
+}
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListSampleActivity.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListSampleActivity.java
new file mode 100644
index 0000000..4eabbdc
--- /dev/null
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/PagedListSampleActivity.java
@@ -0,0 +1,59 @@
+/*
+ * 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 androidx.paging.integration.testapp;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.paging.PagedList;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Sample PagedList activity with artificial data source.
+ */
+public class PagedListSampleActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_recycler_view);
+ final PagedListItemViewModel viewModel = ViewModelProviders.of(this)
+ .get(PagedListItemViewModel.class);
+
+ final PagedListItemAdapter adapter = new PagedListItemAdapter();
+ final RecyclerView recyclerView = findViewById(R.id.recyclerview);
+ recyclerView.setAdapter(adapter);
+ viewModel.getLivePagedList().observe(this, new Observer<PagedList<Item>>() {
+ @Override
+ public void onChanged(@Nullable PagedList<Item> items) {
+ adapter.submitList(items);
+ }
+ });
+ final Button button = findViewById(R.id.button);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ viewModel.invalidateList();
+ }
+ });
+ }
+}
diff --git a/paging/runtime/build.gradle b/paging/runtime/build.gradle
index 20a39d4..dd9189c 100644
--- a/paging/runtime/build.gradle
+++ b/paging/runtime/build.gradle
@@ -25,12 +25,12 @@
}
dependencies {
- api(project(":arch:runtime"))
- api(project(":paging:common"))
- api(project(":lifecycle:runtime"))
- api(project(":lifecycle:livedata"))
+ api(project(":arch:core-runtime"))
+ api(project(":paging:paging-common"))
+ api(project(":lifecycle:lifecycle-runtime"))
+ api(project(":lifecycle:lifecycle-livedata"))
- api(SUPPORT_RECYCLERVIEW_27, libs.support_exclude_config)
+ api(SUPPORT_RECYCLERVIEW, libs.support_exclude_config)
androidTestImplementation(JUNIT)
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/AsyncPagedListDifferTest.kt b/paging/runtime/src/androidTest/java/android/arch/paging/AsyncPagedListDifferTest.kt
deleted file mode 100644
index 586d762..0000000
--- a/paging/runtime/src/androidTest/java/android/arch/paging/AsyncPagedListDifferTest.kt
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- * 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.paging
-
-import android.arch.core.executor.ArchTaskExecutor
-import android.support.test.filters.SmallTest
-import android.support.v7.recyclerview.extensions.AsyncDifferConfig
-import android.support.v7.util.DiffUtil
-import android.support.v7.util.ListUpdateCallback
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
-
-@SmallTest
-@RunWith(JUnit4::class)
-class AsyncPagedListDifferTest {
- private val mMainThread = TestExecutor()
- private val mDiffThread = TestExecutor()
- private val mPageLoadingThread = TestExecutor()
-
- private fun <T> createDiffer(listUpdateCallback: ListUpdateCallback,
- diffCallback: DiffUtil.ItemCallback<T>): AsyncPagedListDiffer<T> {
- val differ = AsyncPagedListDiffer(listUpdateCallback,
- AsyncDifferConfig.Builder<T>(diffCallback)
- .setBackgroundThreadExecutor(mDiffThread)
- .build())
- // by default, use ArchExecutor
- assertEquals(differ.mMainThreadExecutor, ArchTaskExecutor.getMainThreadExecutor())
- differ.mMainThreadExecutor = mMainThread
- return differ
- }
-
- private fun <V> createPagedListFromListAndPos(
- config: PagedList.Config, data: List<V>, initialKey: Int): PagedList<V> {
- return PagedList.Builder<Int, V>(ListDataSource(data), config)
- .setInitialKey(initialKey)
- .setMainThreadExecutor(mMainThread)
- .setBackgroundThreadExecutor(mPageLoadingThread)
- .build()
- }
-
- @Test
- fun initialState() {
- val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
- assertEquals(null, differ.currentList)
- assertEquals(0, differ.itemCount)
- verifyZeroInteractions(callback)
- }
-
- @Test
- fun setFullList() {
- val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
- differ.submitList(StringPagedList(0, 0, "a", "b"))
-
- assertEquals(2, differ.itemCount)
- assertEquals("a", differ.getItem(0))
- assertEquals("b", differ.getItem(1))
-
- verify(callback).onInserted(0, 2)
- verifyNoMoreInteractions(callback)
- drain()
- verifyNoMoreInteractions(callback)
- }
-
- @Test(expected = IndexOutOfBoundsException::class)
- fun getEmpty() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.getItem(0)
- }
-
- @Test(expected = IndexOutOfBoundsException::class)
- fun getNegative() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.submitList(StringPagedList(0, 0, "a", "b"))
- differ.getItem(-1)
- }
-
- @Test(expected = IndexOutOfBoundsException::class)
- fun getPastEnd() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.submitList(StringPagedList(0, 0, "a", "b"))
- differ.getItem(2)
- }
-
- @Test
- fun simpleStatic() {
- val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
-
- assertEquals(0, differ.itemCount)
-
- differ.submitList(StringPagedList(2, 2, "a", "b"))
-
- verify(callback).onInserted(0, 6)
- verifyNoMoreInteractions(callback)
- assertEquals(6, differ.itemCount)
-
- assertNull(differ.getItem(0))
- assertNull(differ.getItem(1))
- assertEquals("a", differ.getItem(2))
- assertEquals("b", differ.getItem(3))
- assertNull(differ.getItem(4))
- assertNull(differ.getItem(5))
- }
-
- @Test
- fun pagingInContent() {
- val config = PagedList.Config.Builder()
- .setInitialLoadSizeHint(4)
- .setPageSize(2)
- .setPrefetchDistance(2)
- .build()
-
- val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
-
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2))
- verify(callback).onInserted(0, ALPHABET_LIST.size)
- verifyNoMoreInteractions(callback)
- drain()
- verifyNoMoreInteractions(callback)
-
- // get without triggering prefetch...
- differ.getItem(1)
- verifyNoMoreInteractions(callback)
- drain()
- verifyNoMoreInteractions(callback)
-
- // get triggering prefetch...
- differ.getItem(2)
- verifyNoMoreInteractions(callback)
- drain()
- verify(callback).onChanged(4, 2, null)
- verifyNoMoreInteractions(callback)
-
- // get with no data loaded nearby...
- differ.getItem(12)
- verifyNoMoreInteractions(callback)
- drain()
- verify(callback).onChanged(10, 2, null)
- verify(callback).onChanged(12, 2, null)
- verify(callback).onChanged(14, 2, null)
- verifyNoMoreInteractions(callback)
-
- // finally, clear
- differ.submitList(null)
- verify(callback).onRemoved(0, 26)
- drain()
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun simpleSwap() {
- // Page size large enough to load
- val config = PagedList.Config.Builder()
- .setPageSize(50)
- .build()
-
- val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
-
- // initial list missing one item (immediate)
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 25), 0))
- verify(callback).onInserted(0, 25)
- verifyNoMoreInteractions(callback)
- assertEquals(differ.itemCount, 25)
- drain()
- verifyNoMoreInteractions(callback)
-
- // pass second list with full data
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 0))
- verifyNoMoreInteractions(callback)
- drain()
- verify(callback).onInserted(25, 1)
- verifyNoMoreInteractions(callback)
- assertEquals(differ.itemCount, 26)
-
- // finally, clear (immediate)
- differ.submitList(null)
- verify(callback).onRemoved(0, 26)
- verifyNoMoreInteractions(callback)
- drain()
- verifyNoMoreInteractions(callback)
- }
-
- @Test
- fun newPageWhileDiffing() {
- val config = PagedList.Config.Builder()
- .setInitialLoadSizeHint(4)
- .setPageSize(2)
- .setPrefetchDistance(2)
- .build()
-
- val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
-
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2))
- verify(callback).onInserted(0, ALPHABET_LIST.size)
- verifyNoMoreInteractions(callback)
- drain()
- verifyNoMoreInteractions(callback)
- assertNotNull(differ.currentList)
- assertFalse(differ.currentList!!.isImmutable)
-
- // trigger page loading
- differ.getItem(10)
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2))
- verifyNoMoreInteractions(callback)
-
- // drain page fetching, but list became immutable, page will be ignored
- drainExceptDiffThread()
- verifyNoMoreInteractions(callback)
- assertNotNull(differ.currentList)
- assertTrue(differ.currentList!!.isImmutable)
-
- // finally full drain, which signals nothing, since 1st pagedlist == 2nd pagedlist
- drain()
- verifyNoMoreInteractions(callback)
- assertNotNull(differ.currentList)
- assertFalse(differ.currentList!!.isImmutable)
- }
-
- @Test
- fun itemCountUpdatedBeforeListUpdateCallbacks() {
- // verify that itemCount is updated in the differ before dispatching ListUpdateCallbacks
-
- val expectedCount = intArrayOf(0)
- // provides access to differ, which must be constructed after callback
- val differAccessor = arrayOf<AsyncPagedListDiffer<*>?>(null)
-
- val callback = object : ListUpdateCallback {
- override fun onInserted(position: Int, count: Int) {
- assertEquals(expectedCount[0], differAccessor[0]!!.itemCount)
- }
-
- override fun onRemoved(position: Int, count: Int) {
- assertEquals(expectedCount[0], differAccessor[0]!!.itemCount)
- }
-
- override fun onMoved(fromPosition: Int, toPosition: Int) {
- fail("not expected")
- }
-
- override fun onChanged(position: Int, count: Int, payload: Any) {
- fail("not expected")
- }
- }
-
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
- differAccessor[0] = differ
-
- val config = PagedList.Config.Builder()
- .setPageSize(20)
- .build()
-
- // in the fast-add case...
- expectedCount[0] = 5
- assertEquals(0, differ.itemCount)
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 5), 0))
- assertEquals(5, differ.itemCount)
-
- // in the slow, diff on BG thread case...
- expectedCount[0] = 10
- assertEquals(5, differ.itemCount)
- differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 10), 0))
- drain()
- assertEquals(10, differ.itemCount)
-
- // and in the fast-remove case
- expectedCount[0] = 0
- assertEquals(10, differ.itemCount)
- differ.submitList(null)
- assertEquals(0, differ.itemCount)
- }
-
- private fun drainExceptDiffThread() {
- var executed: Boolean
- do {
- executed = mPageLoadingThread.executeAll()
- executed = mMainThread.executeAll() or executed
- } while (executed)
- }
-
- private fun drain() {
- var executed: Boolean
- do {
- executed = mPageLoadingThread.executeAll()
- executed = mDiffThread.executeAll() or executed
- executed = mMainThread.executeAll() or executed
- } while (executed)
- }
-
- companion object {
- private val ALPHABET_LIST = List(26) { "" + 'a' + it }
-
- private val STRING_DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
- override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
- return oldItem == newItem
- }
-
- override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
- return oldItem == newItem
- }
- }
-
- private val IGNORE_CALLBACK = object : ListUpdateCallback {
- override fun onInserted(position: Int, count: Int) {}
-
- override fun onRemoved(position: Int, count: Int) {}
-
- override fun onMoved(fromPosition: Int, toPosition: Int) {}
-
- override fun onChanged(position: Int, count: Int, payload: Any) {}
- }
- }
-}
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt b/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt
deleted file mode 100644
index 8b885f2..0000000
--- a/paging/runtime/src/androidTest/java/android/arch/paging/PagedStorageDiffHelperTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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.paging
-
-import android.support.test.filters.SmallTest
-import android.support.v7.util.DiffUtil
-import android.support.v7.util.ListUpdateCallback
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
-
-@SmallTest
-@RunWith(JUnit4::class)
-class PagedStorageDiffHelperTest {
-
- @Test
- fun sameListNoUpdates() {
- validateTwoListDiff(
- PagedStorage(5, listOf("a", "b", "c"), 5),
- PagedStorage(5, listOf("a", "b", "c"), 5)) {
- verifyZeroInteractions(it)
- }
- }
-
- @Test
- fun sameListNoUpdatesPlaceholder() {
- val storageNoPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10)
-
- val storageWithPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10)
- storageWithPlaceholder.allocatePlaceholders(3, 0, 3,
- /* ignored */ mock(PagedStorage.Callback::class.java))
-
- // even though one has placeholders, and null counts are different...
- assertEquals(10, storageNoPlaceholder.trailingNullCount)
- assertEquals(7, storageWithPlaceholder.trailingNullCount)
-
- // ... should be no interactions, since content still same
- validateTwoListDiff(
- storageNoPlaceholder,
- storageWithPlaceholder) {
- verifyZeroInteractions(it)
- }
- }
-
- @Test
- fun appendFill() {
- validateTwoListDiff(
- PagedStorage(5, listOf("a", "b"), 5),
- PagedStorage(5, listOf("a", "b", "c"), 4)) {
- verify(it).onRemoved(11, 1)
- verify(it).onInserted(7, 1)
- // NOTE: ideally would be onChanged(7, 1, null)
- verifyNoMoreInteractions(it)
- }
- }
-
- @Test
- fun prependFill() {
- validateTwoListDiff(
- PagedStorage(5, listOf("b", "c"), 5),
- PagedStorage(4, listOf("a", "b", "c"), 5)) {
- verify(it).onRemoved(0, 1)
- verify(it).onInserted(4, 1)
- //NOTE: ideally would be onChanged(4, 1, null);
- verifyNoMoreInteractions(it)
- }
- }
-
- @Test
- fun change() {
- validateTwoListDiff(
- PagedStorage(5, listOf("a1", "b1", "c1"), 5),
- PagedStorage(5, listOf("a2", "b1", "c2"), 5)) {
- verify(it).onChanged(5, 1, null)
- verify(it).onChanged(7, 1, null)
- verifyNoMoreInteractions(it)
- }
- }
-
- companion object {
- private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
- override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
- // first char means same item
- return oldItem[0] == newItem[0]
- }
-
- override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
- return oldItem == newItem
- }
- }
-
- private fun validateTwoListDiff(oldList: PagedStorage<String>,
- newList: PagedStorage<String>,
- validator: (callback: ListUpdateCallback) -> Unit) {
- val diffResult = PagedStorageDiffHelper.computeDiff(oldList, newList, DIFF_CALLBACK)
-
- val listUpdateCallback = mock(ListUpdateCallback::class.java)
- PagedStorageDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult)
-
- validator(listUpdateCallback)
- }
- }
-}
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt b/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
deleted file mode 100644
index 00e8b0e..0000000
--- a/paging/runtime/src/androidTest/java/android/arch/paging/StringPagedList.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.paging
-
-class StringPagedList constructor(leadingNulls: Int, trailingNulls: Int, vararg items: String)
- : PagedList<String>(PagedStorage<String>(), TestExecutor(), TestExecutor(), null,
- PagedList.Config.Builder().setPageSize(1).build()), PagedStorage.Callback {
- init {
- @Suppress("UNCHECKED_CAST")
- val keyedStorage = mStorage as PagedStorage<String>
- keyedStorage.init(leadingNulls,
- items.toList(),
- trailingNulls,
- 0,
- this)
- }
-
- internal override fun isContiguous(): Boolean {
- return true
- }
-
- override fun getLastKey(): Any? {
- return null
- }
-
- override fun dispatchUpdatesSinceSnapshot(storageSnapshot: PagedList<String>,
- callback: PagedList.Callback) {
- }
-
- override fun loadAroundInternal(index: Int) {}
-
- override fun onInitialized(count: Int) {}
-
- override fun onPagePrepended(leadingNulls: Int, changed: Int, added: Int) {}
-
- override fun onPageAppended(endPosition: Int, changed: Int, added: Int) {}
-
- override fun onPagePlaceholderInserted(pageIndex: Int) {}
-
- override fun onPageInserted(start: Int, count: Int) {}
-
- override fun getDataSource(): DataSource<*, String> {
- throw UnsupportedOperationException()
- }
-}
diff --git a/paging/runtime/src/androidTest/java/android/arch/paging/TestExecutor.kt b/paging/runtime/src/androidTest/java/android/arch/paging/TestExecutor.kt
deleted file mode 100644
index 34e9787..0000000
--- a/paging/runtime/src/androidTest/java/android/arch/paging/TestExecutor.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.paging
-
-import java.util.LinkedList
-import java.util.concurrent.Executor
-
-class TestExecutor : Executor {
- private val mTasks = LinkedList<Runnable>()
-
- override fun execute(command: Runnable) {
- mTasks.add(command)
- }
-
- fun executeAll(): Boolean {
- val consumed = !mTasks.isEmpty()
-
- var task = mTasks.poll()
- while (task != null) {
- task.run()
- task = mTasks.poll()
- }
- return consumed
- }
-}
\ No newline at end of file
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
new file mode 100644
index 0000000..169779d
--- /dev/null
+++ b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
@@ -0,0 +1,342 @@
+/*
+ * 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 androidx.paging
+
+import android.support.test.filters.SmallTest
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListUpdateCallback
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+
+@SmallTest
+@RunWith(JUnit4::class)
+class AsyncPagedListDifferTest {
+ private val mMainThread = TestExecutor()
+ private val mDiffThread = TestExecutor()
+ private val mPageLoadingThread = TestExecutor()
+
+ private fun <T> createDiffer(listUpdateCallback: ListUpdateCallback,
+ diffCallback: DiffUtil.ItemCallback<T>): AsyncPagedListDiffer<T> {
+ val differ = AsyncPagedListDiffer(listUpdateCallback,
+ AsyncDifferConfig.Builder<T>(diffCallback)
+ .setBackgroundThreadExecutor(mDiffThread)
+ .build())
+ // by default, use ArchExecutor
+ assertEquals(differ.mMainThreadExecutor, ArchTaskExecutor.getMainThreadExecutor())
+ differ.mMainThreadExecutor = mMainThread
+ return differ
+ }
+
+ private fun <V> createPagedListFromListAndPos(
+ config: PagedList.Config, data: List<V>, initialKey: Int): PagedList<V> {
+ return PagedList.Builder<Int, V>(ListDataSource(data), config)
+ .setInitialKey(initialKey)
+ .setMainThreadExecutor(mMainThread)
+ .setBackgroundThreadExecutor(mPageLoadingThread)
+ .build()
+ }
+
+ @Test
+ fun initialState() {
+ val callback = mock(ListUpdateCallback::class.java)
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+ assertEquals(null, differ.currentList)
+ assertEquals(0, differ.itemCount)
+ verifyZeroInteractions(callback)
+ }
+
+ @Test
+ fun setFullList() {
+ val callback = mock(ListUpdateCallback::class.java)
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+ differ.submitList(StringPagedList(0, 0, "a", "b"))
+
+ assertEquals(2, differ.itemCount)
+ assertEquals("a", differ.getItem(0))
+ assertEquals("b", differ.getItem(1))
+
+ verify(callback).onInserted(0, 2)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test(expected = IndexOutOfBoundsException::class)
+ fun getEmpty() {
+ val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ differ.getItem(0)
+ }
+
+ @Test(expected = IndexOutOfBoundsException::class)
+ fun getNegative() {
+ val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ differ.submitList(StringPagedList(0, 0, "a", "b"))
+ differ.getItem(-1)
+ }
+
+ @Test(expected = IndexOutOfBoundsException::class)
+ fun getPastEnd() {
+ val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ differ.submitList(StringPagedList(0, 0, "a", "b"))
+ differ.getItem(2)
+ }
+
+ @Test
+ fun simpleStatic() {
+ val callback = mock(ListUpdateCallback::class.java)
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+
+ assertEquals(0, differ.itemCount)
+
+ differ.submitList(StringPagedList(2, 2, "a", "b"))
+
+ verify(callback).onInserted(0, 6)
+ verifyNoMoreInteractions(callback)
+ assertEquals(6, differ.itemCount)
+
+ assertNull(differ.getItem(0))
+ assertNull(differ.getItem(1))
+ assertEquals("a", differ.getItem(2))
+ assertEquals("b", differ.getItem(3))
+ assertNull(differ.getItem(4))
+ assertNull(differ.getItem(5))
+ }
+
+ @Test
+ fun pagingInContent() {
+ val config = PagedList.Config.Builder()
+ .setInitialLoadSizeHint(4)
+ .setPageSize(2)
+ .setPrefetchDistance(2)
+ .build()
+
+ val callback = mock(ListUpdateCallback::class.java)
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2))
+ verify(callback).onInserted(0, ALPHABET_LIST.size)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verifyNoMoreInteractions(callback)
+
+ // get without triggering prefetch...
+ differ.getItem(1)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verifyNoMoreInteractions(callback)
+
+ // get triggering prefetch...
+ differ.getItem(2)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verify(callback).onChanged(4, 2, null)
+ verifyNoMoreInteractions(callback)
+
+ // get with no data loaded nearby...
+ differ.getItem(12)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verify(callback).onChanged(10, 2, null)
+ verify(callback).onChanged(12, 2, null)
+ verify(callback).onChanged(14, 2, null)
+ verifyNoMoreInteractions(callback)
+
+ // finally, clear
+ differ.submitList(null)
+ verify(callback).onRemoved(0, 26)
+ drain()
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun simpleSwap() {
+ // Page size large enough to load
+ val config = PagedList.Config.Builder()
+ .setPageSize(50)
+ .build()
+
+ val callback = mock(ListUpdateCallback::class.java)
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+
+ // initial list missing one item (immediate)
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 25), 0))
+ verify(callback).onInserted(0, 25)
+ verifyNoMoreInteractions(callback)
+ assertEquals(differ.itemCount, 25)
+ drain()
+ verifyNoMoreInteractions(callback)
+
+ // pass second list with full data
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 0))
+ verifyNoMoreInteractions(callback)
+ drain()
+ verify(callback).onInserted(25, 1)
+ verifyNoMoreInteractions(callback)
+ assertEquals(differ.itemCount, 26)
+
+ // finally, clear (immediate)
+ differ.submitList(null)
+ verify(callback).onRemoved(0, 26)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun newPageWhileDiffing() {
+ val config = PagedList.Config.Builder()
+ .setInitialLoadSizeHint(4)
+ .setPageSize(2)
+ .setPrefetchDistance(2)
+ .build()
+
+ val callback = mock(ListUpdateCallback::class.java)
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2))
+ verify(callback).onInserted(0, ALPHABET_LIST.size)
+ verifyNoMoreInteractions(callback)
+ drain()
+ verifyNoMoreInteractions(callback)
+ assertNotNull(differ.currentList)
+ assertFalse(differ.currentList!!.isImmutable)
+
+ // trigger page loading
+ differ.getItem(10)
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2))
+ verifyNoMoreInteractions(callback)
+
+ // drain page fetching, but list became immutable, page will be ignored
+ drainExceptDiffThread()
+ verifyNoMoreInteractions(callback)
+ assertNotNull(differ.currentList)
+ assertTrue(differ.currentList!!.isImmutable)
+
+ // finally full drain, which signals nothing, since 1st pagedlist == 2nd pagedlist
+ drain()
+ verifyNoMoreInteractions(callback)
+ assertNotNull(differ.currentList)
+ assertFalse(differ.currentList!!.isImmutable)
+ }
+
+ @Test
+ fun itemCountUpdatedBeforeListUpdateCallbacks() {
+ // verify that itemCount is updated in the differ before dispatching ListUpdateCallbacks
+
+ val expectedCount = intArrayOf(0)
+ // provides access to differ, which must be constructed after callback
+ val differAccessor = arrayOf<AsyncPagedListDiffer<*>?>(null)
+
+ val callback = object : ListUpdateCallback {
+ override fun onInserted(position: Int, count: Int) {
+ assertEquals(expectedCount[0], differAccessor[0]!!.itemCount)
+ }
+
+ override fun onRemoved(position: Int, count: Int) {
+ assertEquals(expectedCount[0], differAccessor[0]!!.itemCount)
+ }
+
+ override fun onMoved(fromPosition: Int, toPosition: Int) {
+ fail("not expected")
+ }
+
+ override fun onChanged(position: Int, count: Int, payload: Any) {
+ fail("not expected")
+ }
+ }
+
+ val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+ differAccessor[0] = differ
+
+ val config = PagedList.Config.Builder()
+ .setPageSize(20)
+ .build()
+
+ // in the fast-add case...
+ expectedCount[0] = 5
+ assertEquals(0, differ.itemCount)
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 5), 0))
+ assertEquals(5, differ.itemCount)
+
+ // in the slow, diff on BG thread case...
+ expectedCount[0] = 10
+ assertEquals(5, differ.itemCount)
+ differ.submitList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 10), 0))
+ drain()
+ assertEquals(10, differ.itemCount)
+
+ // and in the fast-remove case
+ expectedCount[0] = 0
+ assertEquals(10, differ.itemCount)
+ differ.submitList(null)
+ assertEquals(0, differ.itemCount)
+ }
+
+ private fun drainExceptDiffThread() {
+ var executed: Boolean
+ do {
+ executed = mPageLoadingThread.executeAll()
+ executed = mMainThread.executeAll() or executed
+ } while (executed)
+ }
+
+ private fun drain() {
+ var executed: Boolean
+ do {
+ executed = mPageLoadingThread.executeAll()
+ executed = mDiffThread.executeAll() or executed
+ executed = mMainThread.executeAll() or executed
+ } while (executed)
+ }
+
+ companion object {
+ private val ALPHABET_LIST = List(26) { "" + 'a' + it }
+
+ private val STRING_DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
+ override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+ }
+
+ private val IGNORE_CALLBACK = object : ListUpdateCallback {
+ override fun onInserted(position: Int, count: Int) {}
+
+ override fun onRemoved(position: Int, count: Int) {}
+
+ override fun onMoved(fromPosition: Int, toPosition: Int) {}
+
+ override fun onChanged(position: Int, count: Int, payload: Any) {}
+ }
+ }
+}
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt
new file mode 100644
index 0000000..5bea75b
--- /dev/null
+++ b/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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 androidx.paging
+
+import android.support.test.filters.SmallTest
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListUpdateCallback
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PagedStorageDiffHelperTest {
+
+ @Test
+ fun sameListNoUpdates() {
+ validateTwoListDiff(
+ PagedStorage(5, listOf("a", "b", "c"), 5),
+ PagedStorage(5, listOf("a", "b", "c"), 5)) {
+ verifyZeroInteractions(it)
+ }
+ }
+
+ @Test
+ fun sameListNoUpdatesPlaceholder() {
+ val storageNoPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10)
+
+ val storageWithPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10)
+ storageWithPlaceholder.allocatePlaceholders(3, 0, 3,
+ /* ignored */ mock(PagedStorage.Callback::class.java))
+
+ // even though one has placeholders, and null counts are different...
+ assertEquals(10, storageNoPlaceholder.trailingNullCount)
+ assertEquals(7, storageWithPlaceholder.trailingNullCount)
+
+ // ... should be no interactions, since content still same
+ validateTwoListDiff(
+ storageNoPlaceholder,
+ storageWithPlaceholder) {
+ verifyZeroInteractions(it)
+ }
+ }
+
+ @Test
+ fun appendFill() {
+ validateTwoListDiff(
+ PagedStorage(5, listOf("a", "b"), 5),
+ PagedStorage(5, listOf("a", "b", "c"), 4)) {
+ verify(it).onRemoved(11, 1)
+ verify(it).onInserted(7, 1)
+ // NOTE: ideally would be onChanged(7, 1, null)
+ verifyNoMoreInteractions(it)
+ }
+ }
+
+ @Test
+ fun prependFill() {
+ validateTwoListDiff(
+ PagedStorage(5, listOf("b", "c"), 5),
+ PagedStorage(4, listOf("a", "b", "c"), 5)) {
+ verify(it).onRemoved(0, 1)
+ verify(it).onInserted(4, 1)
+ //NOTE: ideally would be onChanged(4, 1, null);
+ verifyNoMoreInteractions(it)
+ }
+ }
+
+ @Test
+ fun change() {
+ validateTwoListDiff(
+ PagedStorage(5, listOf("a1", "b1", "c1"), 5),
+ PagedStorage(5, listOf("a2", "b1", "c2"), 5)) {
+ verify(it).onChanged(5, 1, null)
+ verify(it).onChanged(7, 1, null)
+ verifyNoMoreInteractions(it)
+ }
+ }
+
+ companion object {
+ private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
+ override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
+ // first char means same item
+ return oldItem[0] == newItem[0]
+ }
+
+ override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
+ return oldItem == newItem
+ }
+ }
+
+ private fun validateTwoListDiff(oldList: PagedStorage<String>,
+ newList: PagedStorage<String>,
+ validator: (callback: ListUpdateCallback) -> Unit) {
+ val diffResult = PagedStorageDiffHelper.computeDiff(oldList, newList, DIFF_CALLBACK)
+
+ val listUpdateCallback = mock(ListUpdateCallback::class.java)
+ PagedStorageDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult)
+
+ validator(listUpdateCallback)
+ }
+ }
+}
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/StringPagedList.kt b/paging/runtime/src/androidTest/java/androidx/paging/StringPagedList.kt
new file mode 100644
index 0000000..fb09091
--- /dev/null
+++ b/paging/runtime/src/androidTest/java/androidx/paging/StringPagedList.kt
@@ -0,0 +1,59 @@
+/*
+ * 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 androidx.paging
+
+class StringPagedList constructor(leadingNulls: Int, trailingNulls: Int, vararg items: String)
+ : PagedList<String>(PagedStorage<String>(), TestExecutor(), TestExecutor(), null,
+ PagedList.Config.Builder().setPageSize(1).build()), PagedStorage.Callback {
+ init {
+ @Suppress("UNCHECKED_CAST")
+ val keyedStorage = mStorage as PagedStorage<String>
+ keyedStorage.init(leadingNulls,
+ items.toList(),
+ trailingNulls,
+ 0,
+ this)
+ }
+
+ internal override fun isContiguous(): Boolean {
+ return true
+ }
+
+ override fun getLastKey(): Any? {
+ return null
+ }
+
+ override fun dispatchUpdatesSinceSnapshot(storageSnapshot: PagedList<String>,
+ callback: PagedList.Callback) {
+ }
+
+ override fun loadAroundInternal(index: Int) {}
+
+ override fun onInitialized(count: Int) {}
+
+ override fun onPagePrepended(leadingNulls: Int, changed: Int, added: Int) {}
+
+ override fun onPageAppended(endPosition: Int, changed: Int, added: Int) {}
+
+ override fun onPagePlaceholderInserted(pageIndex: Int) {}
+
+ override fun onPageInserted(start: Int, count: Int) {}
+
+ override fun getDataSource(): DataSource<*, String> {
+ throw UnsupportedOperationException()
+ }
+}
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/TestExecutor.kt b/paging/runtime/src/androidTest/java/androidx/paging/TestExecutor.kt
new file mode 100644
index 0000000..ac02020
--- /dev/null
+++ b/paging/runtime/src/androidTest/java/androidx/paging/TestExecutor.kt
@@ -0,0 +1,39 @@
+/*
+ * 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 androidx.paging
+
+import java.util.LinkedList
+import java.util.concurrent.Executor
+
+class TestExecutor : Executor {
+ private val mTasks = LinkedList<Runnable>()
+
+ override fun execute(command: Runnable) {
+ mTasks.add(command)
+ }
+
+ fun executeAll(): Boolean {
+ val consumed = !mTasks.isEmpty()
+
+ var task = mTasks.poll()
+ while (task != null) {
+ task.run()
+ task = mTasks.poll()
+ }
+ return consumed
+ }
+}
\ No newline at end of file
diff --git a/paging/runtime/src/main/AndroidManifest.xml b/paging/runtime/src/main/AndroidManifest.xml
index dfc4bdf..cf34bb4 100644
--- a/paging/runtime/src/main/AndroidManifest.xml
+++ b/paging/runtime/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.paging.runtime">
+ package="androidx.paging.runtime">
</manifest>
diff --git a/paging/runtime/src/main/java/android/arch/paging/AsyncPagedListDiffer.java b/paging/runtime/src/main/java/android/arch/paging/AsyncPagedListDiffer.java
deleted file mode 100644
index f00989c..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/AsyncPagedListDiffer.java
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * 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.paging;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.lifecycle.LiveData;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v7.recyclerview.extensions.AsyncDifferConfig;
-import android.support.v7.util.AdapterListUpdateCallback;
-import android.support.v7.util.DiffUtil;
-import android.support.v7.util.ListUpdateCallback;
-import android.support.v7.widget.RecyclerView;
-
-import java.util.concurrent.Executor;
-
-/**
- * Helper object for mapping a {@link PagedList} into a
- * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}.
- * <p>
- * For simplicity, the {@link PagedListAdapter} wrapper class can often be used instead of the
- * differ directly. This diff class is exposed for complex cases, and where overriding an adapter
- * base class to support paging isn't convenient.
- * <p>
- * When consuming a {@link LiveData} of PagedList, you can observe updates and dispatch them
- * directly to {@link #submitList(PagedList)}. The AsyncPagedListDiffer then can present this
- * updating data set simply for an adapter. It listens to PagedList loading callbacks, and uses
- * DiffUtil on a background thread to compute updates as new PagedLists are received.
- * <p>
- * It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an
- * adapter to acquire and present data objects.
- * <p>
- * A complete usage pattern with Room would look like this:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- * public abstract DataSource.Factory<Integer, User> usersByLastName();
- * }
- *
- * class MyViewModel extends ViewModel {
- * public final LiveData<PagedList<User>> usersList;
- * public MyViewModel(UserDao userDao) {
- * usersList = new LivePagedListBuilder<>(
- * userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
- * }
- * }
- *
- * class MyActivity extends AppCompatActivity {
- * {@literal @}Override
- * public void onCreate(Bundle savedState) {
- * super.onCreate(savedState);
- * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
- * RecyclerView recyclerView = findViewById(R.id.user_list);
- * final UserAdapter adapter = new UserAdapter();
- * viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
- * recyclerView.setAdapter(adapter);
- * }
- * }
- *
- * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
- * private final AsyncPagedListDiffer<User> mDiffer
- * = new AsyncPagedListDiffer(this, DIFF_CALLBACK);
- * {@literal @}Override
- * public int getItemCount() {
- * return mDiffer.getItemCount();
- * }
- * public void submitList(PagedList<User> pagedList) {
- * mDiffer.submitList(pagedList);
- * }
- * {@literal @}Override
- * public void onBindViewHolder(UserViewHolder holder, int position) {
- * User user = mDiffer.getItem(position);
- * if (user != null) {
- * holder.bindTo(user);
- * } else {
- * // Null defines a placeholder item - AsyncPagedListDiffer will automatically
- * // invalidate this row when the actual object is loaded from the database
- * holder.clear();
- * }
- * }
- * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
- * new DiffUtil.ItemCallback<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>
- *
- * @param <T> Type of the PagedLists this differ will receive.
- */
-public class AsyncPagedListDiffer<T> {
- // updateCallback notifications must only be notified *after* new data and item count are stored
- // this ensures Adapter#notifyItemRangeInserted etc are accessing the new data
- private final ListUpdateCallback mUpdateCallback;
- private final AsyncDifferConfig<T> mConfig;
-
- @SuppressWarnings("RestrictedApi")
- Executor mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
-
- // TODO: REAL API
- interface PagedListListener<T> {
- void onCurrentListChanged(@Nullable PagedList<T> currentList);
- }
-
- @Nullable
- PagedListListener<T> mListener;
-
- private boolean mIsContiguous;
-
- private PagedList<T> mPagedList;
- private PagedList<T> mSnapshot;
-
- // Max generation of currently scheduled runnable
- private int mMaxScheduledGeneration;
-
- /**
- * Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter),
- * new AsyncDifferConfig.Builder<T>(diffCallback).build();}
- *
- * @param adapter Adapter that will receive update signals.
- * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to
- * compare items in the list.
- */
- @SuppressWarnings("WeakerAccess")
- public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
- @NonNull DiffUtil.ItemCallback<T> diffCallback) {
- mUpdateCallback = new AdapterListUpdateCallback(adapter);
- mConfig = new AsyncDifferConfig.Builder<T>(diffCallback).build();
- }
-
- @SuppressWarnings("WeakerAccess")
- public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
- @NonNull AsyncDifferConfig<T> config) {
- mUpdateCallback = listUpdateCallback;
- mConfig = config;
- }
-
- private PagedList.Callback mPagedListCallback = new PagedList.Callback() {
- @Override
- public void onInserted(int position, int count) {
- mUpdateCallback.onInserted(position, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- mUpdateCallback.onRemoved(position, count);
- }
-
- @Override
- public void onChanged(int position, int count) {
- // NOTE: pass a null payload to convey null -> item
- mUpdateCallback.onChanged(position, count, null);
- }
- };
-
- /**
- * Get the item from the current PagedList at the specified index.
- * <p>
- * Note that this operates on both loaded items and null padding within the PagedList.
- *
- * @param index Index of item to get, must be >= 0, and < {@link #getItemCount()}.
- * @return The item, or null, if a null placeholder is at the specified position.
- */
- @SuppressWarnings("WeakerAccess")
- @Nullable
- public T getItem(int index) {
- if (mPagedList == null) {
- if (mSnapshot == null) {
- throw new IndexOutOfBoundsException(
- "Item count is zero, getItem() call is invalid");
- } else {
- return mSnapshot.get(index);
- }
- }
-
- mPagedList.loadAround(index);
- return mPagedList.get(index);
- }
-
- /**
- * Get the number of items currently presented by this Differ. This value can be directly
- * returned to {@link RecyclerView.Adapter#getItemCount()}.
- *
- * @return Number of items being presented.
- */
- @SuppressWarnings("WeakerAccess")
- public int getItemCount() {
- if (mPagedList != null) {
- return mPagedList.size();
- }
-
- return mSnapshot == null ? 0 : mSnapshot.size();
- }
-
- /**
- * Pass a new PagedList to the differ.
- * <p>
- * If a PagedList is already present, a diff will be computed asynchronously on a background
- * thread. When the diff is computed, it will be applied (dispatched to the
- * {@link ListUpdateCallback}), and the new PagedList will be swapped in as the
- * {@link #getCurrentList() current list}.
- *
- * @param pagedList The new PagedList.
- */
- public void submitList(final PagedList<T> pagedList) {
- if (pagedList != null) {
- if (mPagedList == null && mSnapshot == null) {
- mIsContiguous = pagedList.isContiguous();
- } else {
- if (pagedList.isContiguous() != mIsContiguous) {
- throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both"
- + " contiguous and non-contiguous lists.");
- }
- }
- }
-
- if (pagedList == mPagedList) {
- // nothing to do
- return;
- }
-
- // incrementing generation means any currently-running diffs are discarded when they finish
- final int runGeneration = ++mMaxScheduledGeneration;
-
- if (pagedList == null) {
- int removedCount = getItemCount();
- if (mPagedList != null) {
- mPagedList.removeWeakCallback(mPagedListCallback);
- mPagedList = null;
- } else if (mSnapshot != null) {
- mSnapshot = null;
- }
- // dispatch update callback after updating mPagedList/mSnapshot
- mUpdateCallback.onRemoved(0, removedCount);
- if (mListener != null) {
- mListener.onCurrentListChanged(null);
- }
- return;
- }
-
- if (mPagedList == null && mSnapshot == null) {
- // fast simple first insert
- mPagedList = pagedList;
- pagedList.addWeakCallback(null, mPagedListCallback);
-
- // dispatch update callback after updating mPagedList/mSnapshot
- mUpdateCallback.onInserted(0, pagedList.size());
-
- if (mListener != null) {
- mListener.onCurrentListChanged(pagedList);
- }
- return;
- }
-
- if (mPagedList != null) {
- // first update scheduled on this list, so capture mPages as a snapshot, removing
- // callbacks so we don't have resolve updates against a moving target
- mPagedList.removeWeakCallback(mPagedListCallback);
- mSnapshot = (PagedList<T>) mPagedList.snapshot();
- mPagedList = null;
- }
-
- if (mSnapshot == null || mPagedList != null) {
- throw new IllegalStateException("must be in snapshot state to diff");
- }
-
- final PagedList<T> oldSnapshot = mSnapshot;
- final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
- mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- final DiffUtil.DiffResult result;
- result = PagedStorageDiffHelper.computeDiff(
- oldSnapshot.mStorage,
- newSnapshot.mStorage,
- mConfig.getDiffCallback());
-
- mMainThreadExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (mMaxScheduledGeneration == runGeneration) {
- latchPagedList(pagedList, newSnapshot, result);
- }
- }
- });
- }
- });
- }
-
- private void latchPagedList(
- PagedList<T> newList, PagedList<T> diffSnapshot,
- DiffUtil.DiffResult diffResult) {
- if (mSnapshot == null || mPagedList != null) {
- throw new IllegalStateException("must be in snapshot state to apply diff");
- }
-
- PagedList<T> previousSnapshot = mSnapshot;
- mPagedList = newList;
- mSnapshot = null;
-
- // dispatch update callback after updating mPagedList/mSnapshot
- PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
- previousSnapshot.mStorage, newList.mStorage, diffResult);
-
- newList.addWeakCallback(diffSnapshot, mPagedListCallback);
- if (mListener != null) {
- mListener.onCurrentListChanged(mPagedList);
- }
- }
-
- /**
- * Returns the PagedList currently being displayed by the differ.
- * <p>
- * This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
- * because a diff is computed asynchronously between the new list and the current list before
- * updating the currentList value. May be null if no PagedList is being presented.
- *
- * @return The list currently being displayed, may be null.
- */
- @SuppressWarnings("WeakerAccess")
- @Nullable
- public PagedList<T> getCurrentList() {
- if (mSnapshot != null) {
- return mSnapshot;
- }
- return mPagedList;
- }
-}
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
deleted file mode 100644
index 2cc2026..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/LivePagedListBuilder.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * 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.paging;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.lifecycle.ComputableLiveData;
-import android.arch.lifecycle.LiveData;
-import android.support.annotation.AnyThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.concurrent.Executor;
-
-/**
- * Builder for {@code LiveData<PagedList>}, given a {@link DataSource.Factory} and a
- * {@link PagedList.Config}.
- * <p>
- * The required parameters are in the constructor, so you can simply construct and build, or
- * optionally enable extra features (such as initial load key, or BoundaryCallback.
- *
- * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
- * you're using PositionalDataSource.
- * @param <Value> Item type being presented.
- */
-public final class LivePagedListBuilder<Key, Value> {
- private Key mInitialLoadKey;
- private PagedList.Config mConfig;
- private DataSource.Factory<Key, Value> mDataSourceFactory;
- private PagedList.BoundaryCallback mBoundaryCallback;
- private Executor mBackgroundThreadExecutor;
-
- /**
- * Creates a LivePagedListBuilder with required parameters.
- *
- * @param dataSourceFactory DataSource factory providing DataSource generations.
- * @param config Paging configuration.
- */
- public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
- @NonNull PagedList.Config config) {
- mDataSourceFactory = dataSourceFactory;
- mConfig = config;
- }
-
- /**
- * Creates a LivePagedListBuilder with required parameters.
- * <p>
- * This method is a convenience for:
- * <pre>
- * LivePagedListBuilder(dataSourceFactory,
- * new PagedList.Config.Builder().setPageSize(pageSize).build())
- * </pre>
- *
- * @param dataSourceFactory DataSource.Factory providing DataSource generations.
- * @param pageSize Size of pages to load.
- */
- public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
- int pageSize) {
- this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build());
- }
-
- /**
- * First loading key passed to the first PagedList/DataSource.
- * <p>
- * When a new PagedList/DataSource pair is created after the first, it acquires a load key from
- * the previous generation so that data is loaded around the position already being observed.
- *
- * @param key Initial load key passed to the first PagedList/DataSource.
- * @return this
- */
- @NonNull
- public LivePagedListBuilder<Key, Value> setInitialLoadKey(@Nullable Key key) {
- mInitialLoadKey = key;
- return this;
- }
-
- /**
- * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load
- * additional data from network when paging from local storage.
- * <p>
- * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this
- * method is not called, or {@code null} is passed, you will not be notified when each
- * DataSource runs out of data to provide to its PagedList.
- * <p>
- * If you are paging from a DataSource.Factory backed by local storage, you can set a
- * BoundaryCallback to know when there is no more information to page from local storage.
- * This is useful to page from the network when local storage is a cache of network data.
- * <p>
- * Note that when using a BoundaryCallback with a {@code LiveData<PagedList>}, method calls
- * on the callback may be dispatched multiple times - one for each PagedList/DataSource
- * pair. If loading network data from a BoundaryCallback, you should prevent multiple
- * dispatches of the same method from triggering multiple simultaneous network loads.
- *
- * @param boundaryCallback The boundary callback for listening to PagedList load state.
- * @return this
- */
- @SuppressWarnings("unused")
- @NonNull
- public LivePagedListBuilder<Key, Value> setBoundaryCallback(
- @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
- mBoundaryCallback = boundaryCallback;
- return this;
- }
-
- /**
- * Sets executor which will be used for background loading of pages.
- * <p>
- * If not set, defaults to the Arch components I/O thread.
- * <p>
- * Does not affect initial load, which will be always be done on done on the Arch components
- * I/O thread.
- *
- * @param backgroundThreadExecutor Executor for background DataSource loading.
- * @return this
- */
- @SuppressWarnings("unused")
- @NonNull
- public LivePagedListBuilder<Key, Value> setBackgroundThreadExecutor(
- @NonNull Executor backgroundThreadExecutor) {
- mBackgroundThreadExecutor = backgroundThreadExecutor;
- return this;
- }
-
- /**
- * Constructs the {@code LiveData<PagedList>}.
- * <p>
- * No work (such as loading) is done immediately, the creation of the first PagedList is is
- * deferred until the LiveData is observed.
- *
- * @return The LiveData of PagedLists
- */
- @NonNull
- public LiveData<PagedList<Value>> build() {
- if (mConfig == null) {
- throw new IllegalArgumentException("PagedList.Config must be provided");
- }
- if (mDataSourceFactory == null) {
- throw new IllegalArgumentException("DataSource.Factory must be provided");
- }
- if (mBackgroundThreadExecutor == null) {
- mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor();
- }
-
- return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
- ArchTaskExecutor.getMainThreadExecutor(), mBackgroundThreadExecutor);
- }
-
- @AnyThread
- @NonNull
- private static <Key, Value> LiveData<PagedList<Value>> create(
- @Nullable final Key initialLoadKey,
- @NonNull final PagedList.Config config,
- @Nullable final PagedList.BoundaryCallback boundaryCallback,
- @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
- @NonNull final Executor mainThreadExecutor,
- @NonNull final Executor backgroundThreadExecutor) {
- return new ComputableLiveData<PagedList<Value>>() {
- @Nullable
- private PagedList<Value> mList;
- @Nullable
- private DataSource<Key, Value> mDataSource;
-
- private final DataSource.InvalidatedCallback mCallback =
- new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- }
- };
-
- @Override
- protected PagedList<Value> compute() {
- @Nullable Key initializeKey = initialLoadKey;
- if (mList != null) {
- //noinspection unchecked
- initializeKey = (Key) mList.getLastKey();
- }
-
- do {
- if (mDataSource != null) {
- mDataSource.removeInvalidatedCallback(mCallback);
- }
-
- mDataSource = dataSourceFactory.create();
- mDataSource.addInvalidatedCallback(mCallback);
-
- mList = new PagedList.Builder<>(mDataSource, config)
- .setMainThreadExecutor(mainThreadExecutor)
- .setBackgroundThreadExecutor(backgroundThreadExecutor)
- .setBoundaryCallback(boundaryCallback)
- .setInitialKey(initializeKey)
- .build();
- } while (mList.isDetached());
- return mList;
- }
- }.getLiveData();
- }
-}
diff --git a/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java b/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
deleted file mode 100644
index de470fc..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/LivePagedListProvider.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.RestrictTo;
-import android.support.annotation.WorkerThread;
-
-// NOTE: Room 1.0 depends on this class, so it should not be removed until
-// we can require a version of Room that uses DataSource.Factory directly
-
-/**
- * @hide
- */
-@Deprecated
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class LivePagedListProvider<Key, Value> extends DataSource.Factory<Key, Value> {
-
- @Override
- public DataSource<Key, Value> create() {
- return createDataSource();
- }
-
- @WorkerThread
- protected abstract DataSource<Key, Value> createDataSource();
-}
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java b/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
deleted file mode 100644
index 4728828..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/PagedListAdapter.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v7.recyclerview.extensions.AsyncDifferConfig;
-import android.support.v7.util.AdapterListUpdateCallback;
-import android.support.v7.util.DiffUtil;
-import android.support.v7.widget.RecyclerView;
-
-/**
- * {@link RecyclerView.Adapter RecyclerView.Adapter} base class for presenting paged data from
- * {@link PagedList}s in a {@link RecyclerView}.
- * <p>
- * This class is a convenience wrapper around {@link AsyncPagedListDiffer} that implements common
- * default behavior for item counting, and listening to PagedList update callbacks.
- * <p>
- * While using a LiveData<PagedList> is an easy way to provide data to the adapter, it isn't
- * required - you can use {@link #submitList(PagedList)} when new lists are available.
- * <p>
- * PagedListAdapter listens to PagedList loading callbacks as pages are loaded, and uses DiffUtil on
- * a background thread to compute fine grained updates as new PagedLists are received.
- * <p>
- * Handles both the internal paging of the list as more data is loaded, and updates in the form of
- * new PagedLists.
- * <p>
- * A complete usage pattern with Room would look like this:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- * public abstract DataSource.Factory<Integer, User> usersByLastName();
- * }
- *
- * class MyViewModel extends ViewModel {
- * public final LiveData<PagedList<User>> usersList;
- * public MyViewModel(UserDao userDao) {
- * usersList = new LivePagedListBuilder<>(
- * userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
- * }
- * }
- *
- * class MyActivity extends AppCompatActivity {
- * {@literal @}Override
- * public void onCreate(Bundle savedState) {
- * super.onCreate(savedState);
- * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
- * RecyclerView recyclerView = findViewById(R.id.user_list);
- * UserAdapter<User> adapter = new UserAdapter();
- * viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
- * recyclerView.setAdapter(adapter);
- * }
- * }
- *
- * class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
- * public UserAdapter() {
- * super(DIFF_CALLBACK);
- * }
- * {@literal @}Override
- * public void onBindViewHolder(UserViewHolder holder, int position) {
- * User user = getItem(position);
- * if (user != null) {
- * holder.bindTo(user);
- * } else {
- * // Null defines a placeholder item - PagedListAdapter will automatically invalidate
- * // this row when the actual object is loaded from the database
- * holder.clear();
- * }
- * }
- * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
- * new DiffUtil.ItemCallback<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
- * class should refer to {@link AsyncPagedListDiffer}, which provides the mapping from paging
- * events to adapter-friendly callbacks.
- *
- * @param <T> Type of the PagedLists this Adapter will receive.
- * @param <VH> A class that extends ViewHolder that will be used by the adapter.
- */
-public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
- extends RecyclerView.Adapter<VH> {
- private final AsyncPagedListDiffer<T> mDiffer;
- private final AsyncPagedListDiffer.PagedListListener<T> mListener =
- new AsyncPagedListDiffer.PagedListListener<T>() {
- @Override
- public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
- PagedListAdapter.this.onCurrentListChanged(currentList);
- }
- };
-
- /**
- * Creates a PagedListAdapter with default threading and
- * {@link android.support.v7.util.ListUpdateCallback}.
- *
- * Convenience for {@link #PagedListAdapter(AsyncDifferConfig)}, which uses default threading
- * behavior.
- *
- * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to
- * compare items in the list.
- */
- protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
- mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
- mDiffer.mListener = mListener;
- }
-
- @SuppressWarnings("unused, WeakerAccess")
- protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
- mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
- mDiffer.mListener = mListener;
- }
-
- /**
- * Set the new list to be displayed.
- * <p>
- * If a list is already being displayed, a diff will be computed on a background thread, which
- * will dispatch Adapter.notifyItem events on the main thread.
- *
- * @param pagedList The new list to be displayed.
- */
- public void submitList(PagedList<T> pagedList) {
- mDiffer.submitList(pagedList);
- }
-
- @Nullable
- protected T getItem(int position) {
- return mDiffer.getItem(position);
- }
-
- @Override
- public int getItemCount() {
- return mDiffer.getItemCount();
- }
-
- /**
- * Returns the PagedList currently being displayed by the Adapter.
- * <p>
- * This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
- * because a diff is computed asynchronously between the new list and the current list before
- * updating the currentList value. May be null if no PagedList is being presented.
- *
- * @return The list currently being displayed.
- */
- @Nullable
- public PagedList<T> getCurrentList() {
- return mDiffer.getCurrentList();
- }
-
- /**
- * Called when the current PagedList is updated.
- * <p>
- * This may be dispatched as part of {@link #submitList(PagedList)} if a background diff isn't
- * needed (such as when the first list is passed, or the list is cleared). In either case,
- * PagedListAdapter will simply call
- * {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}.
- * <p>
- * This method will <em>not</em>be called when the Adapter switches from presenting a PagedList
- * to a snapshot version of the PagedList during a diff. This means you cannot observe each
- * PagedList via this method.
- *
- * @param currentList new PagedList being displayed, may be null.
- */
- @SuppressWarnings("WeakerAccess")
- public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
- }
-}
diff --git a/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java b/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
deleted file mode 100644
index f495608..0000000
--- a/paging/runtime/src/main/java/android/arch/paging/PagedStorageDiffHelper.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * 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.paging;
-
-import android.support.annotation.Nullable;
-import android.support.v7.util.DiffUtil;
-import android.support.v7.util.ListUpdateCallback;
-
-class PagedStorageDiffHelper {
- private PagedStorageDiffHelper() {
- }
-
- static <T> DiffUtil.DiffResult computeDiff(
- final PagedStorage<T> oldList,
- final PagedStorage<T> newList,
- final DiffUtil.ItemCallback<T> diffCallback) {
- final int oldOffset = oldList.computeLeadingNulls();
- final int newOffset = newList.computeLeadingNulls();
-
- final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls();
- final int newSize = newList.size() - newOffset - newList.computeTrailingNulls();
-
- return DiffUtil.calculateDiff(new DiffUtil.Callback() {
- @Nullable
- @Override
- public Object getChangePayload(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.get(oldItemPosition + oldOffset);
- T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
- if (oldItem == null || newItem == null) {
- return null;
- }
- return diffCallback.getChangePayload(oldItem, newItem);
- }
-
- @Override
- public int getOldListSize() {
- return oldSize;
- }
-
- @Override
- public int getNewListSize() {
- return newSize;
- }
-
- @Override
- public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.get(oldItemPosition + oldOffset);
- T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
- if (oldItem == newItem) {
- return true;
- }
- //noinspection SimplifiableIfStatement
- if (oldItem == null || newItem == null) {
- return false;
- }
- return diffCallback.areItemsTheSame(oldItem, newItem);
- }
-
- @Override
- public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
- T oldItem = oldList.get(oldItemPosition + oldOffset);
- T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
- if (oldItem == newItem) {
- return true;
- }
- //noinspection SimplifiableIfStatement
- if (oldItem == null || newItem == null) {
- return false;
- }
-
- return diffCallback.areContentsTheSame(oldItem, newItem);
- }
- }, true);
- }
-
- private static class OffsettingListUpdateCallback implements ListUpdateCallback {
- private final int mOffset;
- private final ListUpdateCallback mCallback;
-
- private OffsettingListUpdateCallback(int offset, ListUpdateCallback callback) {
- mOffset = offset;
- mCallback = callback;
- }
-
- @Override
- public void onInserted(int position, int count) {
- mCallback.onInserted(position + mOffset, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- mCallback.onRemoved(position + mOffset, count);
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- mCallback.onRemoved(fromPosition + mOffset, toPosition + mOffset);
- }
-
- @Override
- public void onChanged(int position, int count, Object payload) {
- mCallback.onChanged(position + mOffset, count, payload);
- }
- }
-
- /**
- * TODO: improve diffing logic
- *
- * This function currently does a naive diff, assuming null does not become an item, and vice
- * versa (so it won't dispatch onChange events for these). It's similar to passing a list with
- * leading/trailing nulls in the beginning / end to DiffUtil, but dispatches the remove/insert
- * for changed nulls at the beginning / end of the list.
- *
- * Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we
- * handle this by passing the snapshot to the callback, and dispatching those changes
- * immediately after dispatching this diff.
- */
- static <T> void dispatchDiff(ListUpdateCallback callback,
- final PagedStorage<T> oldList,
- final PagedStorage<T> newList,
- final DiffUtil.DiffResult diffResult) {
-
- final int trailingOld = oldList.computeTrailingNulls();
- final int trailingNew = newList.computeTrailingNulls();
- final int leadingOld = oldList.computeLeadingNulls();
- final int leadingNew = newList.computeLeadingNulls();
-
- if (trailingOld == 0
- && trailingNew == 0
- && leadingOld == 0
- && leadingNew == 0) {
- // Simple case, dispatch & return
- diffResult.dispatchUpdatesTo(callback);
- return;
- }
-
- // First, remove or insert trailing nulls
- if (trailingOld > trailingNew) {
- int count = trailingOld - trailingNew;
- callback.onRemoved(oldList.size() - count, count);
- } else if (trailingOld < trailingNew) {
- callback.onInserted(oldList.size(), trailingNew - trailingOld);
- }
-
- // Second, remove or insert leading nulls
- if (leadingOld > leadingNew) {
- callback.onRemoved(0, leadingOld - leadingNew);
- } else if (leadingOld < leadingNew) {
- callback.onInserted(0, leadingNew - leadingOld);
- }
-
- // apply the diff, with an offset if needed
- if (leadingNew != 0) {
- diffResult.dispatchUpdatesTo(new OffsettingListUpdateCallback(leadingNew, callback));
- } else {
- diffResult.dispatchUpdatesTo(callback);
- }
- }
-}
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
new file mode 100644
index 0000000..f30163d
--- /dev/null
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
@@ -0,0 +1,353 @@
+/*
+ * 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.AdapterListUpdateCallback;
+import androidx.recyclerview.widget.AsyncDifferConfig;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListUpdateCallback;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Helper object for mapping a {@link PagedList} into a
+ * {@link androidx.recyclerview.widget.RecyclerView.Adapter RecyclerView.Adapter}.
+ * <p>
+ * For simplicity, the {@link PagedListAdapter} wrapper class can often be used instead of the
+ * differ directly. This diff class is exposed for complex cases, and where overriding an adapter
+ * base class to support paging isn't convenient.
+ * <p>
+ * When consuming a {@link LiveData} of PagedList, you can observe updates and dispatch them
+ * directly to {@link #submitList(PagedList)}. The AsyncPagedListDiffer then can present this
+ * updating data set simply for an adapter. It listens to PagedList loading callbacks, and uses
+ * DiffUtil on a background thread to compute updates as new PagedLists are received.
+ * <p>
+ * It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an
+ * adapter to acquire and present data objects.
+ * <p>
+ * A complete usage pattern with Room would look like this:
+ * <pre>
+ * {@literal @}Dao
+ * interface UserDao {
+ * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ * public abstract DataSource.Factory<Integer, User> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ * public final LiveData<PagedList<User>> usersList;
+ * public MyViewModel(UserDao userDao) {
+ * usersList = new LivePagedListBuilder<>(
+ * userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
+ * }
+ * }
+ *
+ * class MyActivity extends AppCompatActivity {
+ * {@literal @}Override
+ * public void onCreate(Bundle savedState) {
+ * super.onCreate(savedState);
+ * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
+ * RecyclerView recyclerView = findViewById(R.id.user_list);
+ * final UserAdapter adapter = new UserAdapter();
+ * viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
+ * recyclerView.setAdapter(adapter);
+ * }
+ * }
+ *
+ * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
+ * private final AsyncPagedListDiffer<User> mDiffer
+ * = new AsyncPagedListDiffer(this, DIFF_CALLBACK);
+ * {@literal @}Override
+ * public int getItemCount() {
+ * return mDiffer.getItemCount();
+ * }
+ * public void submitList(PagedList<User> pagedList) {
+ * mDiffer.submitList(pagedList);
+ * }
+ * {@literal @}Override
+ * public void onBindViewHolder(UserViewHolder holder, int position) {
+ * User user = mDiffer.getItem(position);
+ * if (user != null) {
+ * holder.bindTo(user);
+ * } else {
+ * // Null defines a placeholder item - AsyncPagedListDiffer will automatically
+ * // invalidate this row when the actual object is loaded from the database
+ * holder.clear();
+ * }
+ * }
+ * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
+ * new DiffUtil.ItemCallback<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>
+ *
+ * @param <T> Type of the PagedLists this differ will receive.
+ */
+public class AsyncPagedListDiffer<T> {
+ // updateCallback notifications must only be notified *after* new data and item count are stored
+ // this ensures Adapter#notifyItemRangeInserted etc are accessing the new data
+ private final ListUpdateCallback mUpdateCallback;
+ private final AsyncDifferConfig<T> mConfig;
+
+ @SuppressWarnings("RestrictedApi")
+ Executor mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
+
+ // TODO: REAL API
+ interface PagedListListener<T> {
+ void onCurrentListChanged(@Nullable PagedList<T> currentList);
+ }
+
+ @Nullable
+ PagedListListener<T> mListener;
+
+ private boolean mIsContiguous;
+
+ private PagedList<T> mPagedList;
+ private PagedList<T> mSnapshot;
+
+ // Max generation of currently scheduled runnable
+ private int mMaxScheduledGeneration;
+
+ /**
+ * Convenience for {@code AsyncPagedListDiffer(new AdapterListUpdateCallback(adapter),
+ * new AsyncDifferConfig.Builder<T>(diffCallback).build();}
+ *
+ * @param adapter Adapter that will receive update signals.
+ * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to
+ * compare items in the list.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
+ @NonNull DiffUtil.ItemCallback<T> diffCallback) {
+ mUpdateCallback = new AdapterListUpdateCallback(adapter);
+ mConfig = new AsyncDifferConfig.Builder<T>(diffCallback).build();
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
+ @NonNull AsyncDifferConfig<T> config) {
+ mUpdateCallback = listUpdateCallback;
+ mConfig = config;
+ }
+
+ private PagedList.Callback mPagedListCallback = new PagedList.Callback() {
+ @Override
+ public void onInserted(int position, int count) {
+ mUpdateCallback.onInserted(position, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ mUpdateCallback.onRemoved(position, count);
+ }
+
+ @Override
+ public void onChanged(int position, int count) {
+ // NOTE: pass a null payload to convey null -> item
+ mUpdateCallback.onChanged(position, count, null);
+ }
+ };
+
+ /**
+ * Get the item from the current PagedList at the specified index.
+ * <p>
+ * Note that this operates on both loaded items and null padding within the PagedList.
+ *
+ * @param index Index of item to get, must be >= 0, and < {@link #getItemCount()}.
+ * @return The item, or null, if a null placeholder is at the specified position.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public T getItem(int index) {
+ if (mPagedList == null) {
+ if (mSnapshot == null) {
+ throw new IndexOutOfBoundsException(
+ "Item count is zero, getItem() call is invalid");
+ } else {
+ return mSnapshot.get(index);
+ }
+ }
+
+ mPagedList.loadAround(index);
+ return mPagedList.get(index);
+ }
+
+ /**
+ * Get the number of items currently presented by this Differ. This value can be directly
+ * returned to {@link RecyclerView.Adapter#getItemCount()}.
+ *
+ * @return Number of items being presented.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public int getItemCount() {
+ if (mPagedList != null) {
+ return mPagedList.size();
+ }
+
+ return mSnapshot == null ? 0 : mSnapshot.size();
+ }
+
+ /**
+ * Pass a new PagedList to the differ.
+ * <p>
+ * If a PagedList is already present, a diff will be computed asynchronously on a background
+ * thread. When the diff is computed, it will be applied (dispatched to the
+ * {@link ListUpdateCallback}), and the new PagedList will be swapped in as the
+ * {@link #getCurrentList() current list}.
+ *
+ * @param pagedList The new PagedList.
+ */
+ public void submitList(final PagedList<T> pagedList) {
+ if (pagedList != null) {
+ if (mPagedList == null && mSnapshot == null) {
+ mIsContiguous = pagedList.isContiguous();
+ } else {
+ if (pagedList.isContiguous() != mIsContiguous) {
+ throw new IllegalArgumentException("AsyncPagedListDiffer cannot handle both"
+ + " contiguous and non-contiguous lists.");
+ }
+ }
+ }
+
+ if (pagedList == mPagedList) {
+ // nothing to do
+ return;
+ }
+
+ // incrementing generation means any currently-running diffs are discarded when they finish
+ final int runGeneration = ++mMaxScheduledGeneration;
+
+ if (pagedList == null) {
+ int removedCount = getItemCount();
+ if (mPagedList != null) {
+ mPagedList.removeWeakCallback(mPagedListCallback);
+ mPagedList = null;
+ } else if (mSnapshot != null) {
+ mSnapshot = null;
+ }
+ // dispatch update callback after updating mPagedList/mSnapshot
+ mUpdateCallback.onRemoved(0, removedCount);
+ if (mListener != null) {
+ mListener.onCurrentListChanged(null);
+ }
+ return;
+ }
+
+ if (mPagedList == null && mSnapshot == null) {
+ // fast simple first insert
+ mPagedList = pagedList;
+ pagedList.addWeakCallback(null, mPagedListCallback);
+
+ // dispatch update callback after updating mPagedList/mSnapshot
+ mUpdateCallback.onInserted(0, pagedList.size());
+
+ if (mListener != null) {
+ mListener.onCurrentListChanged(pagedList);
+ }
+ return;
+ }
+
+ if (mPagedList != null) {
+ // first update scheduled on this list, so capture mPages as a snapshot, removing
+ // callbacks so we don't have resolve updates against a moving target
+ mPagedList.removeWeakCallback(mPagedListCallback);
+ mSnapshot = (PagedList<T>) mPagedList.snapshot();
+ mPagedList = null;
+ }
+
+ if (mSnapshot == null || mPagedList != null) {
+ throw new IllegalStateException("must be in snapshot state to diff");
+ }
+
+ final PagedList<T> oldSnapshot = mSnapshot;
+ final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot();
+ mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ final DiffUtil.DiffResult result;
+ result = PagedStorageDiffHelper.computeDiff(
+ oldSnapshot.mStorage,
+ newSnapshot.mStorage,
+ mConfig.getDiffCallback());
+
+ mMainThreadExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mMaxScheduledGeneration == runGeneration) {
+ latchPagedList(pagedList, newSnapshot, result);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ private void latchPagedList(
+ PagedList<T> newList, PagedList<T> diffSnapshot,
+ DiffUtil.DiffResult diffResult) {
+ if (mSnapshot == null || mPagedList != null) {
+ throw new IllegalStateException("must be in snapshot state to apply diff");
+ }
+
+ PagedList<T> previousSnapshot = mSnapshot;
+ mPagedList = newList;
+ mSnapshot = null;
+
+ // dispatch update callback after updating mPagedList/mSnapshot
+ PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
+ previousSnapshot.mStorage, newList.mStorage, diffResult);
+
+ newList.addWeakCallback(diffSnapshot, mPagedListCallback);
+ if (mListener != null) {
+ mListener.onCurrentListChanged(mPagedList);
+ }
+ }
+
+ /**
+ * Returns the PagedList currently being displayed by the differ.
+ * <p>
+ * This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
+ * because a diff is computed asynchronously between the new list and the current list before
+ * updating the currentList value. May be null if no PagedList is being presented.
+ *
+ * @return The list currently being displayed, may be null.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public PagedList<T> getCurrentList() {
+ if (mSnapshot != null) {
+ return mSnapshot;
+ }
+ return mPagedList;
+ }
+}
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.java b/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.java
new file mode 100644
index 0000000..0cb0a25
--- /dev/null
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.java
@@ -0,0 +1,211 @@
+/*
+ * 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 androidx.paging;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.lifecycle.ComputableLiveData;
+import androidx.lifecycle.LiveData;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Builder for {@code LiveData<PagedList>}, given a {@link DataSource.Factory} and a
+ * {@link PagedList.Config}.
+ * <p>
+ * The required parameters are in the constructor, so you can simply construct and build, or
+ * optionally enable extra features (such as initial load key, or BoundaryCallback.
+ *
+ * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
+ * you're using PositionalDataSource.
+ * @param <Value> Item type being presented.
+ */
+public final class LivePagedListBuilder<Key, Value> {
+ private Key mInitialLoadKey;
+ private PagedList.Config mConfig;
+ private DataSource.Factory<Key, Value> mDataSourceFactory;
+ private PagedList.BoundaryCallback mBoundaryCallback;
+ private Executor mBackgroundThreadExecutor;
+
+ /**
+ * Creates a LivePagedListBuilder with required parameters.
+ *
+ * @param dataSourceFactory DataSource factory providing DataSource generations.
+ * @param config Paging configuration.
+ */
+ public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+ @NonNull PagedList.Config config) {
+ mDataSourceFactory = dataSourceFactory;
+ mConfig = config;
+ }
+
+ /**
+ * Creates a LivePagedListBuilder with required parameters.
+ * <p>
+ * This method is a convenience for:
+ * <pre>
+ * LivePagedListBuilder(dataSourceFactory,
+ * new PagedList.Config.Builder().setPageSize(pageSize).build())
+ * </pre>
+ *
+ * @param dataSourceFactory DataSource.Factory providing DataSource generations.
+ * @param pageSize Size of pages to load.
+ */
+ public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+ int pageSize) {
+ this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build());
+ }
+
+ /**
+ * First loading key passed to the first PagedList/DataSource.
+ * <p>
+ * When a new PagedList/DataSource pair is created after the first, it acquires a load key from
+ * the previous generation so that data is loaded around the position already being observed.
+ *
+ * @param key Initial load key passed to the first PagedList/DataSource.
+ * @return this
+ */
+ @NonNull
+ public LivePagedListBuilder<Key, Value> setInitialLoadKey(@Nullable Key key) {
+ mInitialLoadKey = key;
+ return this;
+ }
+
+ /**
+ * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load
+ * additional data from network when paging from local storage.
+ * <p>
+ * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this
+ * method is not called, or {@code null} is passed, you will not be notified when each
+ * DataSource runs out of data to provide to its PagedList.
+ * <p>
+ * If you are paging from a DataSource.Factory backed by local storage, you can set a
+ * BoundaryCallback to know when there is no more information to page from local storage.
+ * This is useful to page from the network when local storage is a cache of network data.
+ * <p>
+ * Note that when using a BoundaryCallback with a {@code LiveData<PagedList>}, method calls
+ * on the callback may be dispatched multiple times - one for each PagedList/DataSource
+ * pair. If loading network data from a BoundaryCallback, you should prevent multiple
+ * dispatches of the same method from triggering multiple simultaneous network loads.
+ *
+ * @param boundaryCallback The boundary callback for listening to PagedList load state.
+ * @return this
+ */
+ @SuppressWarnings("unused")
+ @NonNull
+ public LivePagedListBuilder<Key, Value> setBoundaryCallback(
+ @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
+ mBoundaryCallback = boundaryCallback;
+ return this;
+ }
+
+ /**
+ * Sets executor which will be used for background loading of pages.
+ * <p>
+ * If not set, defaults to the Arch components I/O thread.
+ * <p>
+ * Does not affect initial load, which will be always be done on done on the Arch components
+ * I/O thread.
+ *
+ * @param backgroundThreadExecutor Executor for background DataSource loading.
+ * @return this
+ */
+ @SuppressWarnings("unused")
+ @NonNull
+ public LivePagedListBuilder<Key, Value> setBackgroundThreadExecutor(
+ @NonNull Executor backgroundThreadExecutor) {
+ mBackgroundThreadExecutor = backgroundThreadExecutor;
+ return this;
+ }
+
+ /**
+ * Constructs the {@code LiveData<PagedList>}.
+ * <p>
+ * No work (such as loading) is done immediately, the creation of the first PagedList is is
+ * deferred until the LiveData is observed.
+ *
+ * @return The LiveData of PagedLists
+ */
+ @NonNull
+ public LiveData<PagedList<Value>> build() {
+ if (mConfig == null) {
+ throw new IllegalArgumentException("PagedList.Config must be provided");
+ }
+ if (mDataSourceFactory == null) {
+ throw new IllegalArgumentException("DataSource.Factory must be provided");
+ }
+ if (mBackgroundThreadExecutor == null) {
+ mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ }
+
+ return create(mInitialLoadKey, mConfig, mBoundaryCallback, mDataSourceFactory,
+ ArchTaskExecutor.getMainThreadExecutor(), mBackgroundThreadExecutor);
+ }
+
+ @AnyThread
+ @NonNull
+ private static <Key, Value> LiveData<PagedList<Value>> create(
+ @Nullable final Key initialLoadKey,
+ @NonNull final PagedList.Config config,
+ @Nullable final PagedList.BoundaryCallback boundaryCallback,
+ @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
+ @NonNull final Executor mainThreadExecutor,
+ @NonNull final Executor backgroundThreadExecutor) {
+ return new ComputableLiveData<PagedList<Value>>() {
+ @Nullable
+ private PagedList<Value> mList;
+ @Nullable
+ private DataSource<Key, Value> mDataSource;
+
+ private final DataSource.InvalidatedCallback mCallback =
+ new DataSource.InvalidatedCallback() {
+ @Override
+ public void onInvalidated() {
+ invalidate();
+ }
+ };
+
+ @Override
+ protected PagedList<Value> compute() {
+ @Nullable Key initializeKey = initialLoadKey;
+ if (mList != null) {
+ //noinspection unchecked
+ initializeKey = (Key) mList.getLastKey();
+ }
+
+ do {
+ if (mDataSource != null) {
+ mDataSource.removeInvalidatedCallback(mCallback);
+ }
+
+ mDataSource = dataSourceFactory.create();
+ mDataSource.addInvalidatedCallback(mCallback);
+
+ mList = new PagedList.Builder<>(mDataSource, config)
+ .setMainThreadExecutor(mainThreadExecutor)
+ .setBackgroundThreadExecutor(backgroundThreadExecutor)
+ .setBoundaryCallback(boundaryCallback)
+ .setInitialKey(initializeKey)
+ .build();
+ } while (mList.isDetached());
+ return mList;
+ }
+ }.getLiveData();
+ }
+}
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedListProvider.java b/paging/runtime/src/main/java/androidx/paging/LivePagedListProvider.java
new file mode 100644
index 0000000..0fb958d
--- /dev/null
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedListProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.paging;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+
+// NOTE: Room 1.0 depends on this class, so it should not be removed until
+// we can require a version of Room that uses DataSource.Factory directly
+
+/**
+ * @hide
+ * @deprecated Do not use this class.
+ */
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class LivePagedListProvider<Key, Value> extends DataSource.Factory<Key, Value> {
+
+ @Override
+ public DataSource<Key, Value> create() {
+ return createDataSource();
+ }
+
+ @WorkerThread
+ protected abstract DataSource<Key, Value> createDataSource();
+}
diff --git a/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java b/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java
new file mode 100644
index 0000000..72148ad
--- /dev/null
+++ b/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.java
@@ -0,0 +1,195 @@
+/*
+ * 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 androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.AdapterListUpdateCallback;
+import androidx.recyclerview.widget.AsyncDifferConfig;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * {@link RecyclerView.Adapter RecyclerView.Adapter} base class for presenting paged data from
+ * {@link PagedList}s in a {@link RecyclerView}.
+ * <p>
+ * This class is a convenience wrapper around {@link AsyncPagedListDiffer} that implements common
+ * default behavior for item counting, and listening to PagedList update callbacks.
+ * <p>
+ * While using a LiveData<PagedList> is an easy way to provide data to the adapter, it isn't
+ * required - you can use {@link #submitList(PagedList)} when new lists are available.
+ * <p>
+ * PagedListAdapter listens to PagedList loading callbacks as pages are loaded, and uses DiffUtil on
+ * a background thread to compute fine grained updates as new PagedLists are received.
+ * <p>
+ * Handles both the internal paging of the list as more data is loaded, and updates in the form of
+ * new PagedLists.
+ * <p>
+ * A complete usage pattern with Room would look like this:
+ * <pre>
+ * {@literal @}Dao
+ * interface UserDao {
+ * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ * public abstract DataSource.Factory<Integer, User> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ * public final LiveData<PagedList<User>> usersList;
+ * public MyViewModel(UserDao userDao) {
+ * usersList = new LivePagedListBuilder<>(
+ * userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
+ * }
+ * }
+ *
+ * class MyActivity extends AppCompatActivity {
+ * {@literal @}Override
+ * public void onCreate(Bundle savedState) {
+ * super.onCreate(savedState);
+ * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
+ * RecyclerView recyclerView = findViewById(R.id.user_list);
+ * UserAdapter<User> adapter = new UserAdapter();
+ * viewModel.usersList.observe(this, pagedList -> adapter.submitList(pagedList));
+ * recyclerView.setAdapter(adapter);
+ * }
+ * }
+ *
+ * class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
+ * public UserAdapter() {
+ * super(DIFF_CALLBACK);
+ * }
+ * {@literal @}Override
+ * public void onBindViewHolder(UserViewHolder holder, int position) {
+ * User user = getItem(position);
+ * if (user != null) {
+ * holder.bindTo(user);
+ * } else {
+ * // Null defines a placeholder item - PagedListAdapter will automatically invalidate
+ * // this row when the actual object is loaded from the database
+ * holder.clear();
+ * }
+ * }
+ * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
+ * new DiffUtil.ItemCallback<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
+ * class should refer to {@link AsyncPagedListDiffer}, which provides the mapping from paging
+ * events to adapter-friendly callbacks.
+ *
+ * @param <T> Type of the PagedLists this Adapter will receive.
+ * @param <VH> A class that extends ViewHolder that will be used by the adapter.
+ */
+public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
+ extends RecyclerView.Adapter<VH> {
+ private final AsyncPagedListDiffer<T> mDiffer;
+ private final AsyncPagedListDiffer.PagedListListener<T> mListener =
+ new AsyncPagedListDiffer.PagedListListener<T>() {
+ @Override
+ public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
+ PagedListAdapter.this.onCurrentListChanged(currentList);
+ }
+ };
+
+ /**
+ * Creates a PagedListAdapter with default threading and
+ * {@link androidx.recyclerview.widget.ListUpdateCallback}.
+ *
+ * Convenience for {@link #PagedListAdapter(AsyncDifferConfig)}, which uses default threading
+ * behavior.
+ *
+ * @param diffCallback The {@link DiffUtil.ItemCallback DiffUtil.ItemCallback} instance to
+ * compare items in the list.
+ */
+ protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
+ mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
+ mDiffer.mListener = mListener;
+ }
+
+ @SuppressWarnings("unused, WeakerAccess")
+ protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
+ mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
+ mDiffer.mListener = mListener;
+ }
+
+ /**
+ * Set the new list to be displayed.
+ * <p>
+ * If a list is already being displayed, a diff will be computed on a background thread, which
+ * will dispatch Adapter.notifyItem events on the main thread.
+ *
+ * @param pagedList The new list to be displayed.
+ */
+ public void submitList(PagedList<T> pagedList) {
+ mDiffer.submitList(pagedList);
+ }
+
+ @Nullable
+ protected T getItem(int position) {
+ return mDiffer.getItem(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDiffer.getItemCount();
+ }
+
+ /**
+ * Returns the PagedList currently being displayed by the Adapter.
+ * <p>
+ * This is not necessarily the most recent list passed to {@link #submitList(PagedList)},
+ * because a diff is computed asynchronously between the new list and the current list before
+ * updating the currentList value. May be null if no PagedList is being presented.
+ *
+ * @return The list currently being displayed.
+ */
+ @Nullable
+ public PagedList<T> getCurrentList() {
+ return mDiffer.getCurrentList();
+ }
+
+ /**
+ * Called when the current PagedList is updated.
+ * <p>
+ * This may be dispatched as part of {@link #submitList(PagedList)} if a background diff isn't
+ * needed (such as when the first list is passed, or the list is cleared). In either case,
+ * PagedListAdapter will simply call
+ * {@link #notifyItemRangeInserted(int, int) notifyItemRangeInserted/Removed(0, mPreviousSize)}.
+ * <p>
+ * This method will <em>not</em>be called when the Adapter switches from presenting a PagedList
+ * to a snapshot version of the PagedList during a diff. This means you cannot observe each
+ * PagedList via this method.
+ *
+ * @param currentList new PagedList being displayed, may be null.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void onCurrentListChanged(@Nullable PagedList<T> currentList) {
+ }
+}
diff --git a/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java b/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java
new file mode 100644
index 0000000..a2fd8a3
--- /dev/null
+++ b/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.java
@@ -0,0 +1,173 @@
+/*
+ * 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 androidx.paging;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListUpdateCallback;
+
+class PagedStorageDiffHelper {
+ private PagedStorageDiffHelper() {
+ }
+
+ static <T> DiffUtil.DiffResult computeDiff(
+ final PagedStorage<T> oldList,
+ final PagedStorage<T> newList,
+ final DiffUtil.ItemCallback<T> diffCallback) {
+ final int oldOffset = oldList.computeLeadingNulls();
+ final int newOffset = newList.computeLeadingNulls();
+
+ final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls();
+ final int newSize = newList.size() - newOffset - newList.computeTrailingNulls();
+
+ return DiffUtil.calculateDiff(new DiffUtil.Callback() {
+ @Nullable
+ @Override
+ public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+ T oldItem = oldList.get(oldItemPosition + oldOffset);
+ T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
+ if (oldItem == null || newItem == null) {
+ return null;
+ }
+ return diffCallback.getChangePayload(oldItem, newItem);
+ }
+
+ @Override
+ public int getOldListSize() {
+ return oldSize;
+ }
+
+ @Override
+ public int getNewListSize() {
+ return newSize;
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ T oldItem = oldList.get(oldItemPosition + oldOffset);
+ T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
+ if (oldItem == newItem) {
+ return true;
+ }
+ //noinspection SimplifiableIfStatement
+ if (oldItem == null || newItem == null) {
+ return false;
+ }
+ return diffCallback.areItemsTheSame(oldItem, newItem);
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ T oldItem = oldList.get(oldItemPosition + oldOffset);
+ T newItem = newList.get(newItemPosition + newList.getLeadingNullCount());
+ if (oldItem == newItem) {
+ return true;
+ }
+ //noinspection SimplifiableIfStatement
+ if (oldItem == null || newItem == null) {
+ return false;
+ }
+
+ return diffCallback.areContentsTheSame(oldItem, newItem);
+ }
+ }, true);
+ }
+
+ private static class OffsettingListUpdateCallback implements ListUpdateCallback {
+ private final int mOffset;
+ private final ListUpdateCallback mCallback;
+
+ private OffsettingListUpdateCallback(int offset, ListUpdateCallback callback) {
+ mOffset = offset;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onInserted(int position, int count) {
+ mCallback.onInserted(position + mOffset, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ mCallback.onRemoved(position + mOffset, count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ mCallback.onRemoved(fromPosition + mOffset, toPosition + mOffset);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ mCallback.onChanged(position + mOffset, count, payload);
+ }
+ }
+
+ /**
+ * TODO: improve diffing logic
+ *
+ * This function currently does a naive diff, assuming null does not become an item, and vice
+ * versa (so it won't dispatch onChange events for these). It's similar to passing a list with
+ * leading/trailing nulls in the beginning / end to DiffUtil, but dispatches the remove/insert
+ * for changed nulls at the beginning / end of the list.
+ *
+ * Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we
+ * handle this by passing the snapshot to the callback, and dispatching those changes
+ * immediately after dispatching this diff.
+ */
+ static <T> void dispatchDiff(ListUpdateCallback callback,
+ final PagedStorage<T> oldList,
+ final PagedStorage<T> newList,
+ final DiffUtil.DiffResult diffResult) {
+
+ final int trailingOld = oldList.computeTrailingNulls();
+ final int trailingNew = newList.computeTrailingNulls();
+ final int leadingOld = oldList.computeLeadingNulls();
+ final int leadingNew = newList.computeLeadingNulls();
+
+ if (trailingOld == 0
+ && trailingNew == 0
+ && leadingOld == 0
+ && leadingNew == 0) {
+ // Simple case, dispatch & return
+ diffResult.dispatchUpdatesTo(callback);
+ return;
+ }
+
+ // First, remove or insert trailing nulls
+ if (trailingOld > trailingNew) {
+ int count = trailingOld - trailingNew;
+ callback.onRemoved(oldList.size() - count, count);
+ } else if (trailingOld < trailingNew) {
+ callback.onInserted(oldList.size(), trailingNew - trailingOld);
+ }
+
+ // Second, remove or insert leading nulls
+ if (leadingOld > leadingNew) {
+ callback.onRemoved(0, leadingOld - leadingNew);
+ } else if (leadingOld < leadingNew) {
+ callback.onInserted(0, leadingNew - leadingOld);
+ }
+
+ // apply the diff, with an offset if needed
+ if (leadingNew != 0) {
+ diffResult.dispatchUpdatesTo(new OffsettingListUpdateCallback(leadingNew, callback));
+ } else {
+ diffResult.dispatchUpdatesTo(callback);
+ }
+ }
+}
diff --git a/persistence/db-framework/api/current.txt b/persistence/db-framework/api/current.txt
index 7051765..b784c5f 100644
--- a/persistence/db-framework/api/current.txt
+++ b/persistence/db-framework/api/current.txt
@@ -1,8 +1,8 @@
-package android.arch.persistence.db.framework {
+package androidx.sqlite.db.framework {
- public final class FrameworkSQLiteOpenHelperFactory implements android.arch.persistence.db.SupportSQLiteOpenHelper.Factory {
+ public final class FrameworkSQLiteOpenHelperFactory implements androidx.sqlite.db.SupportSQLiteOpenHelper.Factory {
ctor public FrameworkSQLiteOpenHelperFactory();
- method public android.arch.persistence.db.SupportSQLiteOpenHelper create(android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration);
}
}
diff --git a/persistence/db-framework/api/1.0.0.txt b/persistence/db-framework/api_legacy/1.0.0.txt
similarity index 100%
rename from persistence/db-framework/api/1.0.0.txt
rename to persistence/db-framework/api_legacy/1.0.0.txt
diff --git a/persistence/db-framework/api/1.0.0.txt b/persistence/db-framework/api_legacy/1.1.0.txt
similarity index 100%
copy from persistence/db-framework/api/1.0.0.txt
copy to persistence/db-framework/api_legacy/1.1.0.txt
diff --git a/persistence/db-framework/api/1.0.0.txt b/persistence/db-framework/api_legacy/current.txt
similarity index 100%
copy from persistence/db-framework/api/1.0.0.txt
copy to persistence/db-framework/api_legacy/current.txt
diff --git a/persistence/db-framework/build.gradle b/persistence/db-framework/build.gradle
index 15f638b..eb599de 100644
--- a/persistence/db-framework/build.gradle
+++ b/persistence/db-framework/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api(SUPPORT_ANNOTATIONS)
- api(project(":persistence:db"))
+ api(project(":sqlite:sqlite"))
}
supportLibrary {
diff --git a/persistence/db-framework/src/main/AndroidManifest.xml b/persistence/db-framework/src/main/AndroidManifest.xml
index 9350b90..f18f9ad 100644
--- a/persistence/db-framework/src/main/AndroidManifest.xml
+++ b/persistence/db-framework/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.db.framework">
+ package="androidx.sqlite.db.framework">
</manifest>
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
deleted file mode 100644
index d564a03..0000000
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Copyright (C) 2016 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.db.framework;
-
-import static android.text.TextUtils.isEmpty;
-
-import android.arch.persistence.db.SimpleSQLiteQuery;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteCursor;
-import android.database.sqlite.SQLiteCursorDriver;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQuery;
-import android.database.sqlite.SQLiteTransactionListener;
-import android.os.Build;
-import android.os.CancellationSignal;
-import android.support.annotation.RequiresApi;
-import android.util.Pair;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Delegates all calls to an implementation of {@link SQLiteDatabase}.
- */
-@SuppressWarnings("unused")
-class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
- private static final String[] CONFLICT_VALUES = new String[]
- {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
- private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
- private final SQLiteDatabase mDelegate;
-
- /**
- * Creates a wrapper around {@link SQLiteDatabase}.
- *
- * @param delegate The delegate to receive all calls.
- */
- FrameworkSQLiteDatabase(SQLiteDatabase delegate) {
- mDelegate = delegate;
- }
-
- @Override
- public SupportSQLiteStatement compileStatement(String sql) {
- return new FrameworkSQLiteStatement(mDelegate.compileStatement(sql));
- }
-
- @Override
- public void beginTransaction() {
- mDelegate.beginTransaction();
- }
-
- @Override
- public void beginTransactionNonExclusive() {
- mDelegate.beginTransactionNonExclusive();
- }
-
- @Override
- public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
- mDelegate.beginTransactionWithListener(transactionListener);
- }
-
- @Override
- public void beginTransactionWithListenerNonExclusive(
- SQLiteTransactionListener transactionListener) {
- mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
- }
-
- @Override
- public void endTransaction() {
- mDelegate.endTransaction();
- }
-
- @Override
- public void setTransactionSuccessful() {
- mDelegate.setTransactionSuccessful();
- }
-
- @Override
- public boolean inTransaction() {
- return mDelegate.inTransaction();
- }
-
- @Override
- public boolean isDbLockedByCurrentThread() {
- return mDelegate.isDbLockedByCurrentThread();
- }
-
- @Override
- public boolean yieldIfContendedSafely() {
- return mDelegate.yieldIfContendedSafely();
- }
-
- @Override
- public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
- return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
- }
-
- @Override
- public int getVersion() {
- return mDelegate.getVersion();
- }
-
- @Override
- public void setVersion(int version) {
- mDelegate.setVersion(version);
- }
-
- @Override
- public long getMaximumSize() {
- return mDelegate.getMaximumSize();
- }
-
- @Override
- public long setMaximumSize(long numBytes) {
- return mDelegate.setMaximumSize(numBytes);
- }
-
- @Override
- public long getPageSize() {
- return mDelegate.getPageSize();
- }
-
- @Override
- public void setPageSize(long numBytes) {
- mDelegate.setPageSize(numBytes);
- }
-
- @Override
- public Cursor query(String query) {
- return query(new SimpleSQLiteQuery(query));
- }
-
- @Override
- public Cursor query(String query, Object[] bindArgs) {
- return query(new SimpleSQLiteQuery(query, bindArgs));
- }
-
-
- @Override
- public Cursor query(final SupportSQLiteQuery supportQuery) {
- return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
- @Override
- public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
- String editTable, SQLiteQuery query) {
- supportQuery.bindTo(new FrameworkSQLiteProgram(query));
- return new SQLiteCursor(masterQuery, editTable, query);
- }
- }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null);
- }
-
- @Override
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- public Cursor query(final SupportSQLiteQuery supportQuery,
- CancellationSignal cancellationSignal) {
- return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
- @Override
- public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
- String editTable, SQLiteQuery query) {
- supportQuery.bindTo(new FrameworkSQLiteProgram(query));
- return new SQLiteCursor(masterQuery, editTable, query);
- }
- }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null, cancellationSignal);
- }
-
- @Override
- public long insert(String table, int conflictAlgorithm, ContentValues values)
- throws SQLException {
- return mDelegate.insertWithOnConflict(table, null, values,
- conflictAlgorithm);
- }
-
- @Override
- public int delete(String table, String whereClause, Object[] whereArgs) {
- String query = "DELETE FROM " + table
- + (isEmpty(whereClause) ? "" : " WHERE " + whereClause);
- SupportSQLiteStatement statement = compileStatement(query);
- SimpleSQLiteQuery.bind(statement, whereArgs);
- return statement.executeUpdateDelete();
- }
-
-
- @Override
- public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause,
- Object[] whereArgs) {
- // taken from SQLiteDatabase class.
- if (values == null || values.size() == 0) {
- throw new IllegalArgumentException("Empty values");
- }
- StringBuilder sql = new StringBuilder(120);
- sql.append("UPDATE ");
- sql.append(CONFLICT_VALUES[conflictAlgorithm]);
- sql.append(table);
- sql.append(" SET ");
-
- // move all bind args to one array
- int setValuesSize = values.size();
- int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
- Object[] bindArgs = new Object[bindArgsSize];
- int i = 0;
- for (String colName : values.keySet()) {
- sql.append((i > 0) ? "," : "");
- sql.append(colName);
- bindArgs[i++] = values.get(colName);
- sql.append("=?");
- }
- if (whereArgs != null) {
- for (i = setValuesSize; i < bindArgsSize; i++) {
- bindArgs[i] = whereArgs[i - setValuesSize];
- }
- }
- if (!isEmpty(whereClause)) {
- sql.append(" WHERE ");
- sql.append(whereClause);
- }
- SupportSQLiteStatement stmt = compileStatement(sql.toString());
- SimpleSQLiteQuery.bind(stmt, bindArgs);
- return stmt.executeUpdateDelete();
- }
-
- @Override
- public void execSQL(String sql) throws SQLException {
- mDelegate.execSQL(sql);
- }
-
- @Override
- public void execSQL(String sql, Object[] bindArgs) throws SQLException {
- mDelegate.execSQL(sql, bindArgs);
- }
-
- @Override
- public boolean isReadOnly() {
- return mDelegate.isReadOnly();
- }
-
- @Override
- public boolean isOpen() {
- return mDelegate.isOpen();
- }
-
- @Override
- public boolean needUpgrade(int newVersion) {
- return mDelegate.needUpgrade(newVersion);
- }
-
- @Override
- public String getPath() {
- return mDelegate.getPath();
- }
-
- @Override
- public void setLocale(Locale locale) {
- mDelegate.setLocale(locale);
- }
-
- @Override
- public void setMaxSqlCacheSize(int cacheSize) {
- mDelegate.setMaxSqlCacheSize(cacheSize);
- }
-
- @Override
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- public void setForeignKeyConstraintsEnabled(boolean enable) {
- mDelegate.setForeignKeyConstraintsEnabled(enable);
- }
-
- @Override
- public boolean enableWriteAheadLogging() {
- return mDelegate.enableWriteAheadLogging();
- }
-
- @Override
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- public void disableWriteAheadLogging() {
- mDelegate.disableWriteAheadLogging();
- }
-
- @Override
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- public boolean isWriteAheadLoggingEnabled() {
- return mDelegate.isWriteAheadLoggingEnabled();
- }
-
- @Override
- public List<Pair<String, String>> getAttachedDbs() {
- return mDelegate.getAttachedDbs();
- }
-
- @Override
- public boolean isDatabaseIntegrityOk() {
- return mDelegate.isDatabaseIntegrityOk();
- }
-
- @Override
- public void close() throws IOException {
- mDelegate.close();
- }
-}
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java
deleted file mode 100644
index a1690f4..0000000
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2016 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.db.framework;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.content.Context;
-import android.database.DatabaseErrorHandler;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.Build;
-import android.support.annotation.RequiresApi;
-
-class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
- private final OpenHelper mDelegate;
-
- FrameworkSQLiteOpenHelper(Context context, String name,
- Callback callback) {
- mDelegate = createDelegate(context, name, callback);
- }
-
- private OpenHelper createDelegate(Context context, String name, Callback callback) {
- final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1];
- return new OpenHelper(context, name, dbRef, callback);
- }
-
- @Override
- public String getDatabaseName() {
- return mDelegate.getDatabaseName();
- }
-
- @Override
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- public void setWriteAheadLoggingEnabled(boolean enabled) {
- mDelegate.setWriteAheadLoggingEnabled(enabled);
- }
-
- @Override
- public SupportSQLiteDatabase getWritableDatabase() {
- return mDelegate.getWritableSupportDatabase();
- }
-
- @Override
- public SupportSQLiteDatabase getReadableDatabase() {
- return mDelegate.getReadableSupportDatabase();
- }
-
- @Override
- public void close() {
- mDelegate.close();
- }
-
- 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;
-
- 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() {
- SQLiteDatabase db = super.getWritableDatabase();
- return getWrappedDb(db);
- }
-
- SupportSQLiteDatabase getReadableSupportDatabase() {
- SQLiteDatabase db = super.getReadableDatabase();
- return getWrappedDb(db);
- }
-
- FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) {
- FrameworkSQLiteDatabase dbRef = mDbRef[0];
- if (dbRef == null) {
- dbRef = new FrameworkSQLiteDatabase(sqLiteDatabase);
- mDbRef[0] = dbRef;
- }
- 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();
- mDbRef[0] = null;
- }
- }
-}
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java
deleted file mode 100644
index ab11d49..0000000
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2016 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.db.framework;
-
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-
-/**
- * Implements {@link SupportSQLiteOpenHelper.Factory} using the SQLite implementation in the
- * framework.
- */
-@SuppressWarnings("unused")
-public final class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
- @Override
- public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
- return new FrameworkSQLiteOpenHelper(
- configuration.context, configuration.name, configuration.callback);
- }
-}
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java
deleted file mode 100644
index 73c98c6..0000000
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.db.framework;
-
-import android.arch.persistence.db.SupportSQLiteProgram;
-import android.database.sqlite.SQLiteProgram;
-
-/**
- * An wrapper around {@link SQLiteProgram} to implement {@link SupportSQLiteProgram} API.
- */
-class FrameworkSQLiteProgram implements SupportSQLiteProgram {
- private final SQLiteProgram mDelegate;
-
- FrameworkSQLiteProgram(SQLiteProgram delegate) {
- mDelegate = delegate;
- }
-
- @Override
- public void bindNull(int index) {
- mDelegate.bindNull(index);
- }
-
- @Override
- public void bindLong(int index, long value) {
- mDelegate.bindLong(index, value);
- }
-
- @Override
- public void bindDouble(int index, double value) {
- mDelegate.bindDouble(index, value);
- }
-
- @Override
- public void bindString(int index, String value) {
- mDelegate.bindString(index, value);
- }
-
- @Override
- public void bindBlob(int index, byte[] value) {
- mDelegate.bindBlob(index, value);
- }
-
- @Override
- public void clearBindings() {
- mDelegate.clearBindings();
- }
-
- @Override
- public void close() {
- mDelegate.close();
- }
-}
diff --git a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
deleted file mode 100644
index 7f07865..0000000
--- a/persistence/db-framework/src/main/java/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2016 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.db.framework;
-
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.database.sqlite.SQLiteStatement;
-
-/**
- * Delegates all calls to a {@link SQLiteStatement}.
- */
-class FrameworkSQLiteStatement extends FrameworkSQLiteProgram implements SupportSQLiteStatement {
- private final SQLiteStatement mDelegate;
-
- /**
- * Creates a wrapper around a framework {@link SQLiteStatement}.
- *
- * @param delegate The SQLiteStatement to delegate calls to.
- */
- FrameworkSQLiteStatement(SQLiteStatement delegate) {
- super(delegate);
- mDelegate = delegate;
- }
-
- @Override
- public void execute() {
- mDelegate.execute();
- }
-
- @Override
- public int executeUpdateDelete() {
- return mDelegate.executeUpdateDelete();
- }
-
- @Override
- public long executeInsert() {
- return mDelegate.executeInsert();
- }
-
- @Override
- public long simpleQueryForLong() {
- return mDelegate.simpleQueryForLong();
- }
-
- @Override
- public String simpleQueryForString() {
- return mDelegate.simpleQueryForString();
- }
-}
diff --git a/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.java b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.java
new file mode 100644
index 0000000..15a4a2e
--- /dev/null
+++ b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db.framework;
+
+import static android.text.TextUtils.isEmpty;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteCursorDriver;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQuery;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Delegates all calls to an implementation of {@link SQLiteDatabase}.
+ */
+@SuppressWarnings("unused")
+class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
+ private static final String[] CONFLICT_VALUES = new String[]
+ {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private final SQLiteDatabase mDelegate;
+
+ /**
+ * Creates a wrapper around {@link SQLiteDatabase}.
+ *
+ * @param delegate The delegate to receive all calls.
+ */
+ FrameworkSQLiteDatabase(SQLiteDatabase delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public SupportSQLiteStatement compileStatement(String sql) {
+ return new FrameworkSQLiteStatement(mDelegate.compileStatement(sql));
+ }
+
+ @Override
+ public void beginTransaction() {
+ mDelegate.beginTransaction();
+ }
+
+ @Override
+ public void beginTransactionNonExclusive() {
+ mDelegate.beginTransactionNonExclusive();
+ }
+
+ @Override
+ public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
+ mDelegate.beginTransactionWithListener(transactionListener);
+ }
+
+ @Override
+ public void beginTransactionWithListenerNonExclusive(
+ SQLiteTransactionListener transactionListener) {
+ mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
+ }
+
+ @Override
+ public void endTransaction() {
+ mDelegate.endTransaction();
+ }
+
+ @Override
+ public void setTransactionSuccessful() {
+ mDelegate.setTransactionSuccessful();
+ }
+
+ @Override
+ public boolean inTransaction() {
+ return mDelegate.inTransaction();
+ }
+
+ @Override
+ public boolean isDbLockedByCurrentThread() {
+ return mDelegate.isDbLockedByCurrentThread();
+ }
+
+ @Override
+ public boolean yieldIfContendedSafely() {
+ return mDelegate.yieldIfContendedSafely();
+ }
+
+ @Override
+ public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
+ return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
+ }
+
+ @Override
+ public int getVersion() {
+ return mDelegate.getVersion();
+ }
+
+ @Override
+ public void setVersion(int version) {
+ mDelegate.setVersion(version);
+ }
+
+ @Override
+ public long getMaximumSize() {
+ return mDelegate.getMaximumSize();
+ }
+
+ @Override
+ public long setMaximumSize(long numBytes) {
+ return mDelegate.setMaximumSize(numBytes);
+ }
+
+ @Override
+ public long getPageSize() {
+ return mDelegate.getPageSize();
+ }
+
+ @Override
+ public void setPageSize(long numBytes) {
+ mDelegate.setPageSize(numBytes);
+ }
+
+ @Override
+ public Cursor query(String query) {
+ return query(new SimpleSQLiteQuery(query));
+ }
+
+ @Override
+ public Cursor query(String query, Object[] bindArgs) {
+ return query(new SimpleSQLiteQuery(query, bindArgs));
+ }
+
+
+ @Override
+ public Cursor query(final SupportSQLiteQuery supportQuery) {
+ return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
+ @Override
+ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
+ String editTable, SQLiteQuery query) {
+ supportQuery.bindTo(new FrameworkSQLiteProgram(query));
+ return new SQLiteCursor(masterQuery, editTable, query);
+ }
+ }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null);
+ }
+
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public Cursor query(final SupportSQLiteQuery supportQuery,
+ CancellationSignal cancellationSignal) {
+ return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
+ @Override
+ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
+ String editTable, SQLiteQuery query) {
+ supportQuery.bindTo(new FrameworkSQLiteProgram(query));
+ return new SQLiteCursor(masterQuery, editTable, query);
+ }
+ }, supportQuery.getSql(), EMPTY_STRING_ARRAY, null, cancellationSignal);
+ }
+
+ @Override
+ public long insert(String table, int conflictAlgorithm, ContentValues values)
+ throws SQLException {
+ return mDelegate.insertWithOnConflict(table, null, values,
+ conflictAlgorithm);
+ }
+
+ @Override
+ public int delete(String table, String whereClause, Object[] whereArgs) {
+ String query = "DELETE FROM " + table
+ + (isEmpty(whereClause) ? "" : " WHERE " + whereClause);
+ SupportSQLiteStatement statement = compileStatement(query);
+ SimpleSQLiteQuery.bind(statement, whereArgs);
+ return statement.executeUpdateDelete();
+ }
+
+
+ @Override
+ public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause,
+ Object[] whereArgs) {
+ // taken from SQLiteDatabase class.
+ if (values == null || values.size() == 0) {
+ throw new IllegalArgumentException("Empty values");
+ }
+ StringBuilder sql = new StringBuilder(120);
+ sql.append("UPDATE ");
+ sql.append(CONFLICT_VALUES[conflictAlgorithm]);
+ sql.append(table);
+ sql.append(" SET ");
+
+ // move all bind args to one array
+ int setValuesSize = values.size();
+ int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
+ Object[] bindArgs = new Object[bindArgsSize];
+ int i = 0;
+ for (String colName : values.keySet()) {
+ sql.append((i > 0) ? "," : "");
+ sql.append(colName);
+ bindArgs[i++] = values.get(colName);
+ sql.append("=?");
+ }
+ if (whereArgs != null) {
+ for (i = setValuesSize; i < bindArgsSize; i++) {
+ bindArgs[i] = whereArgs[i - setValuesSize];
+ }
+ }
+ if (!isEmpty(whereClause)) {
+ sql.append(" WHERE ");
+ sql.append(whereClause);
+ }
+ SupportSQLiteStatement stmt = compileStatement(sql.toString());
+ SimpleSQLiteQuery.bind(stmt, bindArgs);
+ return stmt.executeUpdateDelete();
+ }
+
+ @Override
+ public void execSQL(String sql) throws SQLException {
+ mDelegate.execSQL(sql);
+ }
+
+ @Override
+ public void execSQL(String sql, Object[] bindArgs) throws SQLException {
+ mDelegate.execSQL(sql, bindArgs);
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return mDelegate.isReadOnly();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return mDelegate.isOpen();
+ }
+
+ @Override
+ public boolean needUpgrade(int newVersion) {
+ return mDelegate.needUpgrade(newVersion);
+ }
+
+ @Override
+ public String getPath() {
+ return mDelegate.getPath();
+ }
+
+ @Override
+ public void setLocale(Locale locale) {
+ mDelegate.setLocale(locale);
+ }
+
+ @Override
+ public void setMaxSqlCacheSize(int cacheSize) {
+ mDelegate.setMaxSqlCacheSize(cacheSize);
+ }
+
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public void setForeignKeyConstraintsEnabled(boolean enable) {
+ mDelegate.setForeignKeyConstraintsEnabled(enable);
+ }
+
+ @Override
+ public boolean enableWriteAheadLogging() {
+ return mDelegate.enableWriteAheadLogging();
+ }
+
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public void disableWriteAheadLogging() {
+ mDelegate.disableWriteAheadLogging();
+ }
+
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public boolean isWriteAheadLoggingEnabled() {
+ return mDelegate.isWriteAheadLoggingEnabled();
+ }
+
+ @Override
+ public List<Pair<String, String>> getAttachedDbs() {
+ return mDelegate.getAttachedDbs();
+ }
+
+ @Override
+ public boolean isDatabaseIntegrityOk() {
+ return mDelegate.isDatabaseIntegrityOk();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mDelegate.close();
+ }
+}
diff --git a/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java
new file mode 100644
index 0000000..9ae374e
--- /dev/null
+++ b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db.framework;
+
+import android.content.Context;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
+ private final OpenHelper mDelegate;
+
+ FrameworkSQLiteOpenHelper(Context context, String name,
+ Callback callback) {
+ mDelegate = createDelegate(context, name, callback);
+ }
+
+ private OpenHelper createDelegate(Context context, String name, Callback callback) {
+ final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1];
+ return new OpenHelper(context, name, dbRef, callback);
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return mDelegate.getDatabaseName();
+ }
+
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public void setWriteAheadLoggingEnabled(boolean enabled) {
+ mDelegate.setWriteAheadLoggingEnabled(enabled);
+ }
+
+ @Override
+ public SupportSQLiteDatabase getWritableDatabase() {
+ return mDelegate.getWritableSupportDatabase();
+ }
+
+ @Override
+ public SupportSQLiteDatabase getReadableDatabase() {
+ return mDelegate.getReadableSupportDatabase();
+ }
+
+ @Override
+ public void close() {
+ mDelegate.close();
+ }
+
+ 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;
+
+ 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() {
+ SQLiteDatabase db = super.getWritableDatabase();
+ return getWrappedDb(db);
+ }
+
+ SupportSQLiteDatabase getReadableSupportDatabase() {
+ SQLiteDatabase db = super.getReadableDatabase();
+ return getWrappedDb(db);
+ }
+
+ FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) {
+ FrameworkSQLiteDatabase dbRef = mDbRef[0];
+ if (dbRef == null) {
+ dbRef = new FrameworkSQLiteDatabase(sqLiteDatabase);
+ mDbRef[0] = dbRef;
+ }
+ 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();
+ mDbRef[0] = null;
+ }
+ }
+}
diff --git a/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java
new file mode 100644
index 0000000..e65fabe
--- /dev/null
+++ b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db.framework;
+
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+/**
+ * Implements {@link SupportSQLiteOpenHelper.Factory} using the SQLite implementation in the
+ * framework.
+ */
+@SuppressWarnings("unused")
+public final class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+ @Override
+ public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
+ return new FrameworkSQLiteOpenHelper(
+ configuration.context, configuration.name, configuration.callback);
+ }
+}
diff --git a/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteProgram.java b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteProgram.java
new file mode 100644
index 0000000..d352d56
--- /dev/null
+++ b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteProgram.java
@@ -0,0 +1,67 @@
+/*
+ * 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 androidx.sqlite.db.framework;
+
+import android.database.sqlite.SQLiteProgram;
+
+import androidx.sqlite.db.SupportSQLiteProgram;
+
+/**
+ * An wrapper around {@link SQLiteProgram} to implement {@link SupportSQLiteProgram} API.
+ */
+class FrameworkSQLiteProgram implements SupportSQLiteProgram {
+ private final SQLiteProgram mDelegate;
+
+ FrameworkSQLiteProgram(SQLiteProgram delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void bindNull(int index) {
+ mDelegate.bindNull(index);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ mDelegate.bindLong(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ mDelegate.bindDouble(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ mDelegate.bindString(index, value);
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ mDelegate.bindBlob(index, value);
+ }
+
+ @Override
+ public void clearBindings() {
+ mDelegate.clearBindings();
+ }
+
+ @Override
+ public void close() {
+ mDelegate.close();
+ }
+}
diff --git a/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteStatement.java b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteStatement.java
new file mode 100644
index 0000000..9ef2fa7
--- /dev/null
+++ b/persistence/db-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteStatement.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db.framework;
+
+import android.database.sqlite.SQLiteStatement;
+
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+/**
+ * Delegates all calls to a {@link SQLiteStatement}.
+ */
+class FrameworkSQLiteStatement extends FrameworkSQLiteProgram implements SupportSQLiteStatement {
+ private final SQLiteStatement mDelegate;
+
+ /**
+ * Creates a wrapper around a framework {@link SQLiteStatement}.
+ *
+ * @param delegate The SQLiteStatement to delegate calls to.
+ */
+ FrameworkSQLiteStatement(SQLiteStatement delegate) {
+ super(delegate);
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void execute() {
+ mDelegate.execute();
+ }
+
+ @Override
+ public int executeUpdateDelete() {
+ return mDelegate.executeUpdateDelete();
+ }
+
+ @Override
+ public long executeInsert() {
+ return mDelegate.executeInsert();
+ }
+
+ @Override
+ public long simpleQueryForLong() {
+ return mDelegate.simpleQueryForLong();
+ }
+
+ @Override
+ public String simpleQueryForString() {
+ return mDelegate.simpleQueryForString();
+ }
+}
diff --git a/persistence/db/api/current.txt b/persistence/db/api/current.txt
index e0abe20..270d88c 100644
--- a/persistence/db/api/current.txt
+++ b/persistence/db/api/current.txt
@@ -1,10 +1,10 @@
-package android.arch.persistence.db {
+package androidx.sqlite.db {
- public final class SimpleSQLiteQuery implements android.arch.persistence.db.SupportSQLiteQuery {
+ public final class SimpleSQLiteQuery implements androidx.sqlite.db.SupportSQLiteQuery {
ctor public SimpleSQLiteQuery(java.lang.String, java.lang.Object[]);
ctor public SimpleSQLiteQuery(java.lang.String);
- method public static void bind(android.arch.persistence.db.SupportSQLiteProgram, java.lang.Object[]);
- method public void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public static void bind(androidx.sqlite.db.SupportSQLiteProgram, java.lang.Object[]);
+ method public void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
method public int getArgCount();
method public java.lang.String getSql();
}
@@ -14,7 +14,7 @@
method public abstract void beginTransactionNonExclusive();
method public abstract void beginTransactionWithListener(android.database.sqlite.SQLiteTransactionListener);
method public abstract void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener);
- method public abstract android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method public abstract androidx.sqlite.db.SupportSQLiteStatement compileStatement(java.lang.String);
method public abstract int delete(java.lang.String, java.lang.String, java.lang.Object[]);
method public abstract void disableWriteAheadLogging();
method public abstract boolean enableWriteAheadLogging();
@@ -36,8 +36,8 @@
method public abstract boolean needUpgrade(int);
method public abstract android.database.Cursor query(java.lang.String);
method public abstract android.database.Cursor query(java.lang.String, java.lang.Object[]);
- method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
- method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery, android.os.CancellationSignal);
+ method public abstract android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery);
+ method public abstract android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery, android.os.CancellationSignal);
method public abstract void setForeignKeyConstraintsEnabled(boolean);
method public abstract void setLocale(java.util.Locale);
method public abstract void setMaxSqlCacheSize(int);
@@ -53,37 +53,37 @@
public abstract interface SupportSQLiteOpenHelper {
method public abstract void close();
method public abstract java.lang.String getDatabaseName();
- method public abstract android.arch.persistence.db.SupportSQLiteDatabase getReadableDatabase();
- method public abstract android.arch.persistence.db.SupportSQLiteDatabase getWritableDatabase();
+ method public abstract androidx.sqlite.db.SupportSQLiteDatabase getReadableDatabase();
+ method public abstract androidx.sqlite.db.SupportSQLiteDatabase getWritableDatabase();
method public abstract void setWriteAheadLoggingEnabled(boolean);
}
public static abstract class SupportSQLiteOpenHelper.Callback {
ctor public SupportSQLiteOpenHelper.Callback(int);
- method public void onConfigure(android.arch.persistence.db.SupportSQLiteDatabase);
- method public void onCorruption(android.arch.persistence.db.SupportSQLiteDatabase);
- method public abstract void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
- method public void onDowngrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
- method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
- method public abstract void onUpgrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase);
+ method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase);
+ method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase);
+ method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
+ method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase);
+ method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
field public final int version;
}
public static class SupportSQLiteOpenHelper.Configuration {
- method public static android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
- field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Callback callback;
+ method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+ field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final java.lang.String name;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
- method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration build();
- method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder callback(android.arch.persistence.db.SupportSQLiteOpenHelper.Callback);
- method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder name(java.lang.String);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(java.lang.String);
}
public static abstract interface SupportSQLiteOpenHelper.Factory {
- method public abstract android.arch.persistence.db.SupportSQLiteOpenHelper create(android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration);
+ method public abstract androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration);
}
public abstract interface SupportSQLiteProgram implements java.io.Closeable {
@@ -96,24 +96,24 @@
}
public abstract interface SupportSQLiteQuery {
- method public abstract void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public abstract void bindTo(androidx.sqlite.db.SupportSQLiteProgram);
method public abstract int getArgCount();
method public abstract java.lang.String getSql();
}
public final class SupportSQLiteQueryBuilder {
- method public static android.arch.persistence.db.SupportSQLiteQueryBuilder builder(java.lang.String);
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder columns(java.lang.String[]);
- method public android.arch.persistence.db.SupportSQLiteQuery create();
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder distinct();
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder groupBy(java.lang.String);
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder having(java.lang.String);
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder limit(java.lang.String);
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder orderBy(java.lang.String);
- method public android.arch.persistence.db.SupportSQLiteQueryBuilder selection(java.lang.String, java.lang.Object[]);
+ method public static androidx.sqlite.db.SupportSQLiteQueryBuilder builder(java.lang.String);
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder columns(java.lang.String[]);
+ method public androidx.sqlite.db.SupportSQLiteQuery create();
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder distinct();
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder groupBy(java.lang.String);
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder having(java.lang.String);
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder limit(java.lang.String);
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder orderBy(java.lang.String);
+ method public androidx.sqlite.db.SupportSQLiteQueryBuilder selection(java.lang.String, java.lang.Object[]);
}
- public abstract interface SupportSQLiteStatement implements android.arch.persistence.db.SupportSQLiteProgram {
+ public abstract interface SupportSQLiteStatement implements androidx.sqlite.db.SupportSQLiteProgram {
method public abstract void execute();
method public abstract long executeInsert();
method public abstract int executeUpdateDelete();
diff --git a/persistence/db/api/1.0.0.txt b/persistence/db/api_legacy/1.0.0.txt
similarity index 100%
rename from persistence/db/api/1.0.0.txt
rename to persistence/db/api_legacy/1.0.0.txt
diff --git a/persistence/db/api_legacy/1.1.0.txt b/persistence/db/api_legacy/1.1.0.txt
new file mode 100644
index 0000000..e0abe20
--- /dev/null
+++ b/persistence/db/api_legacy/1.1.0.txt
@@ -0,0 +1,125 @@
+package android.arch.persistence.db {
+
+ public final class SimpleSQLiteQuery implements android.arch.persistence.db.SupportSQLiteQuery {
+ ctor public SimpleSQLiteQuery(java.lang.String, java.lang.Object[]);
+ ctor public SimpleSQLiteQuery(java.lang.String);
+ method public static void bind(android.arch.persistence.db.SupportSQLiteProgram, java.lang.Object[]);
+ method public void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public int getArgCount();
+ method public java.lang.String getSql();
+ }
+
+ public abstract interface SupportSQLiteDatabase implements java.io.Closeable {
+ method public abstract void beginTransaction();
+ method public abstract void beginTransactionNonExclusive();
+ method public abstract void beginTransactionWithListener(android.database.sqlite.SQLiteTransactionListener);
+ method public abstract void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener);
+ method public abstract android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method public abstract int delete(java.lang.String, java.lang.String, java.lang.Object[]);
+ method public abstract void disableWriteAheadLogging();
+ method public abstract boolean enableWriteAheadLogging();
+ method public abstract void endTransaction();
+ method public abstract void execSQL(java.lang.String) throws android.database.SQLException;
+ method public abstract void execSQL(java.lang.String, java.lang.Object[]) throws android.database.SQLException;
+ method public abstract java.util.List<android.util.Pair<java.lang.String, java.lang.String>> getAttachedDbs();
+ method public abstract long getMaximumSize();
+ method public abstract long getPageSize();
+ method public abstract java.lang.String getPath();
+ method public abstract int getVersion();
+ method public abstract boolean inTransaction();
+ method public abstract long insert(java.lang.String, int, android.content.ContentValues) throws android.database.SQLException;
+ method public abstract boolean isDatabaseIntegrityOk();
+ method public abstract boolean isDbLockedByCurrentThread();
+ method public abstract boolean isOpen();
+ method public abstract boolean isReadOnly();
+ method public abstract boolean isWriteAheadLoggingEnabled();
+ method public abstract boolean needUpgrade(int);
+ method public abstract android.database.Cursor query(java.lang.String);
+ method public abstract android.database.Cursor query(java.lang.String, java.lang.Object[]);
+ method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery, android.os.CancellationSignal);
+ method public abstract void setForeignKeyConstraintsEnabled(boolean);
+ method public abstract void setLocale(java.util.Locale);
+ method public abstract void setMaxSqlCacheSize(int);
+ method public abstract long setMaximumSize(long);
+ method public abstract void setPageSize(long);
+ method public abstract void setTransactionSuccessful();
+ method public abstract void setVersion(int);
+ method public abstract int update(java.lang.String, int, android.content.ContentValues, java.lang.String, java.lang.Object[]);
+ method public abstract boolean yieldIfContendedSafely();
+ method public abstract boolean yieldIfContendedSafely(long);
+ }
+
+ public abstract interface SupportSQLiteOpenHelper {
+ method public abstract void close();
+ method public abstract java.lang.String getDatabaseName();
+ method public abstract android.arch.persistence.db.SupportSQLiteDatabase getReadableDatabase();
+ method public abstract android.arch.persistence.db.SupportSQLiteDatabase getWritableDatabase();
+ method public abstract void setWriteAheadLoggingEnabled(boolean);
+ }
+
+ public static abstract class SupportSQLiteOpenHelper.Callback {
+ ctor public SupportSQLiteOpenHelper.Callback(int);
+ method public void onConfigure(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onCorruption(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onDowngrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void onUpgrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ field public final int version;
+ }
+
+ public static class SupportSQLiteOpenHelper.Configuration {
+ method public static android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+ field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Callback callback;
+ field public final android.content.Context context;
+ field public final java.lang.String name;
+ }
+
+ public static class SupportSQLiteOpenHelper.Configuration.Builder {
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration build();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder callback(android.arch.persistence.db.SupportSQLiteOpenHelper.Callback);
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder name(java.lang.String);
+ }
+
+ public static abstract interface SupportSQLiteOpenHelper.Factory {
+ method public abstract android.arch.persistence.db.SupportSQLiteOpenHelper create(android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration);
+ }
+
+ public abstract interface SupportSQLiteProgram implements java.io.Closeable {
+ method public abstract void bindBlob(int, byte[]);
+ method public abstract void bindDouble(int, double);
+ method public abstract void bindLong(int, long);
+ method public abstract void bindNull(int);
+ method public abstract void bindString(int, java.lang.String);
+ method public abstract void clearBindings();
+ }
+
+ public abstract interface SupportSQLiteQuery {
+ method public abstract void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public abstract int getArgCount();
+ method public abstract java.lang.String getSql();
+ }
+
+ public final class SupportSQLiteQueryBuilder {
+ method public static android.arch.persistence.db.SupportSQLiteQueryBuilder builder(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder columns(java.lang.String[]);
+ method public android.arch.persistence.db.SupportSQLiteQuery create();
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder distinct();
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder groupBy(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder having(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder limit(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder orderBy(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder selection(java.lang.String, java.lang.Object[]);
+ }
+
+ public abstract interface SupportSQLiteStatement implements android.arch.persistence.db.SupportSQLiteProgram {
+ method public abstract void execute();
+ method public abstract long executeInsert();
+ method public abstract int executeUpdateDelete();
+ method public abstract long simpleQueryForLong();
+ method public abstract java.lang.String simpleQueryForString();
+ }
+
+}
+
diff --git a/persistence/db/api_legacy/current.txt b/persistence/db/api_legacy/current.txt
new file mode 100644
index 0000000..e0abe20
--- /dev/null
+++ b/persistence/db/api_legacy/current.txt
@@ -0,0 +1,125 @@
+package android.arch.persistence.db {
+
+ public final class SimpleSQLiteQuery implements android.arch.persistence.db.SupportSQLiteQuery {
+ ctor public SimpleSQLiteQuery(java.lang.String, java.lang.Object[]);
+ ctor public SimpleSQLiteQuery(java.lang.String);
+ method public static void bind(android.arch.persistence.db.SupportSQLiteProgram, java.lang.Object[]);
+ method public void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public int getArgCount();
+ method public java.lang.String getSql();
+ }
+
+ public abstract interface SupportSQLiteDatabase implements java.io.Closeable {
+ method public abstract void beginTransaction();
+ method public abstract void beginTransactionNonExclusive();
+ method public abstract void beginTransactionWithListener(android.database.sqlite.SQLiteTransactionListener);
+ method public abstract void beginTransactionWithListenerNonExclusive(android.database.sqlite.SQLiteTransactionListener);
+ method public abstract android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method public abstract int delete(java.lang.String, java.lang.String, java.lang.Object[]);
+ method public abstract void disableWriteAheadLogging();
+ method public abstract boolean enableWriteAheadLogging();
+ method public abstract void endTransaction();
+ method public abstract void execSQL(java.lang.String) throws android.database.SQLException;
+ method public abstract void execSQL(java.lang.String, java.lang.Object[]) throws android.database.SQLException;
+ method public abstract java.util.List<android.util.Pair<java.lang.String, java.lang.String>> getAttachedDbs();
+ method public abstract long getMaximumSize();
+ method public abstract long getPageSize();
+ method public abstract java.lang.String getPath();
+ method public abstract int getVersion();
+ method public abstract boolean inTransaction();
+ method public abstract long insert(java.lang.String, int, android.content.ContentValues) throws android.database.SQLException;
+ method public abstract boolean isDatabaseIntegrityOk();
+ method public abstract boolean isDbLockedByCurrentThread();
+ method public abstract boolean isOpen();
+ method public abstract boolean isReadOnly();
+ method public abstract boolean isWriteAheadLoggingEnabled();
+ method public abstract boolean needUpgrade(int);
+ method public abstract android.database.Cursor query(java.lang.String);
+ method public abstract android.database.Cursor query(java.lang.String, java.lang.Object[]);
+ method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public abstract android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery, android.os.CancellationSignal);
+ method public abstract void setForeignKeyConstraintsEnabled(boolean);
+ method public abstract void setLocale(java.util.Locale);
+ method public abstract void setMaxSqlCacheSize(int);
+ method public abstract long setMaximumSize(long);
+ method public abstract void setPageSize(long);
+ method public abstract void setTransactionSuccessful();
+ method public abstract void setVersion(int);
+ method public abstract int update(java.lang.String, int, android.content.ContentValues, java.lang.String, java.lang.Object[]);
+ method public abstract boolean yieldIfContendedSafely();
+ method public abstract boolean yieldIfContendedSafely(long);
+ }
+
+ public abstract interface SupportSQLiteOpenHelper {
+ method public abstract void close();
+ method public abstract java.lang.String getDatabaseName();
+ method public abstract android.arch.persistence.db.SupportSQLiteDatabase getReadableDatabase();
+ method public abstract android.arch.persistence.db.SupportSQLiteDatabase getWritableDatabase();
+ method public abstract void setWriteAheadLoggingEnabled(boolean);
+ }
+
+ public static abstract class SupportSQLiteOpenHelper.Callback {
+ ctor public SupportSQLiteOpenHelper.Callback(int);
+ method public void onConfigure(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onCorruption(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onDowngrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void onUpgrade(android.arch.persistence.db.SupportSQLiteDatabase, int, int);
+ field public final int version;
+ }
+
+ public static class SupportSQLiteOpenHelper.Configuration {
+ method public static android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+ field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Callback callback;
+ field public final android.content.Context context;
+ field public final java.lang.String name;
+ }
+
+ public static class SupportSQLiteOpenHelper.Configuration.Builder {
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration build();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder callback(android.arch.persistence.db.SupportSQLiteOpenHelper.Callback);
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration.Builder name(java.lang.String);
+ }
+
+ public static abstract interface SupportSQLiteOpenHelper.Factory {
+ method public abstract android.arch.persistence.db.SupportSQLiteOpenHelper create(android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration);
+ }
+
+ public abstract interface SupportSQLiteProgram implements java.io.Closeable {
+ method public abstract void bindBlob(int, byte[]);
+ method public abstract void bindDouble(int, double);
+ method public abstract void bindLong(int, long);
+ method public abstract void bindNull(int);
+ method public abstract void bindString(int, java.lang.String);
+ method public abstract void clearBindings();
+ }
+
+ public abstract interface SupportSQLiteQuery {
+ method public abstract void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+ method public abstract int getArgCount();
+ method public abstract java.lang.String getSql();
+ }
+
+ public final class SupportSQLiteQueryBuilder {
+ method public static android.arch.persistence.db.SupportSQLiteQueryBuilder builder(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder columns(java.lang.String[]);
+ method public android.arch.persistence.db.SupportSQLiteQuery create();
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder distinct();
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder groupBy(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder having(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder limit(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder orderBy(java.lang.String);
+ method public android.arch.persistence.db.SupportSQLiteQueryBuilder selection(java.lang.String, java.lang.Object[]);
+ }
+
+ public abstract interface SupportSQLiteStatement implements android.arch.persistence.db.SupportSQLiteProgram {
+ method public abstract void execute();
+ method public abstract long executeInsert();
+ method public abstract int executeUpdateDelete();
+ method public abstract long simpleQueryForLong();
+ method public abstract java.lang.String simpleQueryForString();
+ }
+
+}
+
diff --git a/persistence/db/src/main/AndroidManifest.xml b/persistence/db/src/main/AndroidManifest.xml
index 8a27324..8b4263b 100644
--- a/persistence/db/src/main/AndroidManifest.xml
+++ b/persistence/db/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.db">
+ package="androidx.sqlite.db">
</manifest>
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
deleted file mode 100644
index 13faf24..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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.db;
-
-/**
- * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
- * binds args based on the passed in Object type.
- */
-public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
- private final String mQuery;
- private final Object[] mBindArgs;
-
- /**
- * Creates an SQL query with the sql string and the bind arguments.
- *
- * @param query The query string, can include bind arguments (.e.g ?).
- * @param bindArgs The bind argument value that will replace the placeholders in the query.
- */
- public SimpleSQLiteQuery(String query, Object[] bindArgs) {
- mQuery = query;
- mBindArgs = bindArgs;
- }
-
- /**
- * Creates an SQL query without any bind arguments.
- *
- * @param query The SQL query to execute. Cannot include bind parameters.
- */
- public SimpleSQLiteQuery(String query) {
- this(query, null);
- }
-
- @Override
- public String getSql() {
- return mQuery;
- }
-
- @Override
- public void bindTo(SupportSQLiteProgram statement) {
- bind(statement, mBindArgs);
- }
-
- @Override
- public int getArgCount() {
- return mBindArgs.length;
- }
-
- /**
- * Binds the given arguments into the given sqlite statement.
- *
- * @param statement The sqlite statement
- * @param bindArgs The list of bind arguments
- */
- public static void bind(SupportSQLiteProgram statement, Object[] bindArgs) {
- if (bindArgs == null) {
- return;
- }
- final int limit = bindArgs.length;
- for (int i = 0; i < limit; i++) {
- final Object arg = bindArgs[i];
- bind(statement, i + 1, arg);
- }
- }
-
- private static void bind(SupportSQLiteProgram statement, int index, Object arg) {
- // extracted from android.database.sqlite.SQLiteConnection
- if (arg == null) {
- statement.bindNull(index);
- } else if (arg instanceof byte[]) {
- statement.bindBlob(index, (byte[]) arg);
- } else if (arg instanceof Float) {
- statement.bindDouble(index, (Float) arg);
- } else if (arg instanceof Double) {
- statement.bindDouble(index, (Double) arg);
- } else if (arg instanceof Long) {
- statement.bindLong(index, (Long) arg);
- } else if (arg instanceof Integer) {
- statement.bindLong(index, (Integer) arg);
- } else if (arg instanceof Short) {
- statement.bindLong(index, (Short) arg);
- } else if (arg instanceof Byte) {
- statement.bindLong(index, (Byte) arg);
- } else if (arg instanceof String) {
- statement.bindString(index, (String) arg);
- } else if (arg instanceof Boolean) {
- statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
- } else {
- throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
- + " Supported types: null, byte[], float, double, long, int, short, byte,"
- + " string");
- }
- }
-}
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteDatabase.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteDatabase.java
deleted file mode 100644
index 5f71baf..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteDatabase.java
+++ /dev/null
@@ -1,605 +0,0 @@
-/*
- * Copyright (C) 2016 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.db;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteTransactionListener;
-import android.os.Build;
-import android.os.CancellationSignal;
-import android.os.OperationCanceledException;
-import android.support.annotation.RequiresApi;
-import android.util.Pair;
-
-import java.io.Closeable;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * A database abstraction which removes the framework dependency and allows swapping underlying
- * sql versions. It mimics the behavior of {@link android.database.sqlite.SQLiteDatabase}
- */
-@SuppressWarnings("unused")
-public interface SupportSQLiteDatabase extends Closeable {
- /**
- * Compiles the given SQL statement.
- *
- * @param sql The sql query.
- * @return Compiled statement.
- */
- SupportSQLiteStatement compileStatement(String sql);
-
- /**
- * Begins a transaction in EXCLUSIVE mode.
- * <p>
- * Transactions can be nested.
- * When the outer transaction is ended all of
- * the work done in that transaction and all of the nested transactions will be committed or
- * rolled back. The changes will be rolled back if any transaction is ended without being
- * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
- * </p>
- * <p>Here is the standard idiom for transactions:
- *
- * <pre>
- * db.beginTransaction();
- * try {
- * ...
- * db.setTransactionSuccessful();
- * } finally {
- * db.endTransaction();
- * }
- * </pre>
- */
- void beginTransaction();
-
- /**
- * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When
- * the outer transaction is ended all of the work done in that transaction
- * and all of the nested transactions will be committed or rolled back. The
- * changes will be rolled back if any transaction is ended without being
- * marked as clean (by calling setTransactionSuccessful). Otherwise they
- * will be committed.
- * <p>
- * Here is the standard idiom for transactions:
- *
- * <pre>
- * db.beginTransactionNonExclusive();
- * try {
- * ...
- * db.setTransactionSuccessful();
- * } finally {
- * db.endTransaction();
- * }
- * </pre>
- */
- void beginTransactionNonExclusive();
-
- /**
- * Begins a transaction in EXCLUSIVE mode.
- * <p>
- * Transactions can be nested.
- * When the outer transaction is ended all of
- * the work done in that transaction and all of the nested transactions will be committed or
- * rolled back. The changes will be rolled back if any transaction is ended without being
- * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
- * </p>
- * <p>Here is the standard idiom for transactions:
- *
- * <pre>
- * db.beginTransactionWithListener(listener);
- * try {
- * ...
- * db.setTransactionSuccessful();
- * } finally {
- * db.endTransaction();
- * }
- * </pre>
- *
- * @param transactionListener listener that should be notified when the transaction begins,
- * commits, or is rolled back, either explicitly or by a call to
- * {@link #yieldIfContendedSafely}.
- */
- void beginTransactionWithListener(SQLiteTransactionListener transactionListener);
-
- /**
- * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When
- * the outer transaction is ended all of the work done in that transaction
- * and all of the nested transactions will be committed or rolled back. The
- * changes will be rolled back if any transaction is ended without being
- * marked as clean (by calling setTransactionSuccessful). Otherwise they
- * will be committed.
- * <p>
- * Here is the standard idiom for transactions:
- *
- * <pre>
- * db.beginTransactionWithListenerNonExclusive(listener);
- * try {
- * ...
- * db.setTransactionSuccessful();
- * } finally {
- * db.endTransaction();
- * }
- * </pre>
- *
- * @param transactionListener listener that should be notified when the
- * transaction begins, commits, or is rolled back, either
- * explicitly or by a call to {@link #yieldIfContendedSafely}.
- */
- void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener);
-
- /**
- * End a transaction. See beginTransaction for notes about how to use this and when transactions
- * are committed and rolled back.
- */
- void endTransaction();
-
- /**
- * Marks the current transaction as successful. Do not do any more database work between
- * calling this and calling endTransaction. Do as little non-database work as possible in that
- * situation too. If any errors are encountered between this and endTransaction the transaction
- * will still be committed.
- *
- * @throws IllegalStateException if the current thread is not in a transaction or the
- * transaction is already marked as successful.
- */
- void setTransactionSuccessful();
-
- /**
- * Returns true if the current thread has a transaction pending.
- *
- * @return True if the current thread is in a transaction.
- */
- boolean inTransaction();
-
- /**
- * Returns true if the current thread is holding an active connection to the database.
- * <p>
- * The name of this method comes from a time when having an active connection
- * to the database meant that the thread was holding an actual lock on the
- * database. Nowadays, there is no longer a true "database lock" although threads
- * may block if they cannot acquire a database connection to perform a
- * particular operation.
- * </p>
- *
- * @return True if the current thread is holding an active connection to the database.
- */
- boolean isDbLockedByCurrentThread();
-
- /**
- * Temporarily end the transaction to let other threads run. The transaction is assumed to be
- * successful so far. Do not call setTransactionSuccessful before calling this. When this
- * returns a new transaction will have been created but not marked as successful. This assumes
- * that there are no nested transactions (beginTransaction has only been called once) and will
- * throw an exception if that is not the case.
- *
- * @return true if the transaction was yielded
- */
- boolean yieldIfContendedSafely();
-
- /**
- * Temporarily end the transaction to let other threads run. The transaction is assumed to be
- * successful so far. Do not call setTransactionSuccessful before calling this. When this
- * returns a new transaction will have been created but not marked as successful. This assumes
- * that there are no nested transactions (beginTransaction has only been called once) and will
- * throw an exception if that is not the case.
- *
- * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if
- * the lock was actually yielded. This will allow other background
- * threads to make some
- * more progress than they would if we started the transaction
- * immediately.
- * @return true if the transaction was yielded
- */
- boolean yieldIfContendedSafely(long sleepAfterYieldDelay);
-
- /**
- * Gets the database version.
- *
- * @return the database version
- */
- int getVersion();
-
- /**
- * Sets the database version.
- *
- * @param version the new database version
- */
- void setVersion(int version);
-
- /**
- * Returns the maximum size the database may grow to.
- *
- * @return the new maximum database size
- */
- long getMaximumSize();
-
- /**
- * Sets the maximum size the database will grow to. The maximum size cannot
- * be set below the current size.
- *
- * @param numBytes the maximum database size, in bytes
- * @return the new maximum database size
- */
- long setMaximumSize(long numBytes);
-
- /**
- * Returns the current database page size, in bytes.
- *
- * @return the database page size, in bytes
- */
- long getPageSize();
-
- /**
- * Sets the database page size. The page size must be a power of two. This
- * method does not work if any data has been written to the database file,
- * and must be called right after the database has been created.
- *
- * @param numBytes the database page size, in bytes
- */
- void setPageSize(long numBytes);
-
- /**
- * Runs the given query on the database. If you would like to have typed bind arguments,
- * use {@link #query(SupportSQLiteQuery)}.
- *
- * @param query The SQL query that includes the query and can bind into a given compiled
- * program.
- * @return A {@link Cursor} object, which is positioned before the first entry. Note that
- * {@link Cursor}s are not synchronized, see the documentation for more details.
- * @see #query(SupportSQLiteQuery)
- */
- Cursor query(String query);
-
- /**
- * Runs the given query on the database. If you would like to have bind arguments,
- * use {@link #query(SupportSQLiteQuery)}.
- *
- * @param query The SQL query that includes the query and can bind into a given compiled
- * program.
- * @param bindArgs The query arguments to bind.
- * @return A {@link Cursor} object, which is positioned before the first entry. Note that
- * {@link Cursor}s are not synchronized, see the documentation for more details.
- * @see #query(SupportSQLiteQuery)
- */
- Cursor query(String query, Object[] bindArgs);
-
- /**
- * Runs the given query on the database.
- * <p>
- * This class allows using type safe sql program bindings while running queries.
- *
- * @param query The SQL query that includes the query and can bind into a given compiled
- * program.
- * @return A {@link Cursor} object, which is positioned before the first entry. Note that
- * {@link Cursor}s are not synchronized, see the documentation for more details.
- * @see SimpleSQLiteQuery
- */
- Cursor query(SupportSQLiteQuery query);
-
- /**
- * Runs the given query on the database.
- * <p>
- * This class allows using type safe sql program bindings while running queries.
- *
- * @param query The SQL query that includes the query and can bind into a given compiled
- * program.
- * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
- * If the operation is canceled, then {@link OperationCanceledException} will be thrown
- * when the query is executed.
- * @return A {@link Cursor} object, which is positioned before the first entry. Note that
- * {@link Cursor}s are not synchronized, see the documentation for more details.
- */
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal);
-
- /**
- * Convenience method for inserting a row into the database.
- *
- * @param table the table to insert the row into
- * @param values this map contains the initial column values for the
- * row. The keys should be the column names and the values the
- * column values
- * @param conflictAlgorithm for insert conflict resolver. One of
- * {@link SQLiteDatabase#CONFLICT_NONE}, {@link SQLiteDatabase#CONFLICT_ROLLBACK},
- * {@link SQLiteDatabase#CONFLICT_ABORT}, {@link SQLiteDatabase#CONFLICT_FAIL},
- * {@link SQLiteDatabase#CONFLICT_IGNORE}, {@link SQLiteDatabase#CONFLICT_REPLACE}.
- * @return the row ID of the newly inserted row, or -1 if an error occurred
- * @throws SQLException If the insert fails
- */
- long insert(String table, int conflictAlgorithm, ContentValues values) throws SQLException;
-
- /**
- * Convenience method for deleting rows in the database.
- *
- * @param table the table to delete from
- * @param whereClause the optional WHERE clause to apply when deleting.
- * Passing null will delete all rows.
- * @param whereArgs You may include ?s in the where clause, which
- * will be replaced by the values from whereArgs. The values
- * will be bound as Strings.
- * @return the number of rows affected if a whereClause is passed in, 0
- * otherwise. To remove all rows and get a count pass "1" as the
- * whereClause.
- */
- int delete(String table, String whereClause, Object[] whereArgs);
-
- /**
- * Convenience method for updating rows in the database.
- *
- * @param table the table to update in
- * @param conflictAlgorithm for update conflict resolver. One of
- * {@link SQLiteDatabase#CONFLICT_NONE}, {@link SQLiteDatabase#CONFLICT_ROLLBACK},
- * {@link SQLiteDatabase#CONFLICT_ABORT}, {@link SQLiteDatabase#CONFLICT_FAIL},
- * {@link SQLiteDatabase#CONFLICT_IGNORE}, {@link SQLiteDatabase#CONFLICT_REPLACE}.
- * @param values a map from column names to new column values. null is a
- * valid value that will be translated to NULL.
- * @param whereClause the optional WHERE clause to apply when updating.
- * Passing null will update all rows.
- * @param whereArgs You may include ?s in the where clause, which
- * will be replaced by the values from whereArgs. The values
- * will be bound as Strings.
- * @return the number of rows affected
- */
- int update(String table, int conflictAlgorithm,
- ContentValues values, String whereClause, Object[] whereArgs);
-
- /**
- * Execute a single SQL statement that does not return any data.
- * <p>
- * When using {@link #enableWriteAheadLogging()}, journal_mode is
- * automatically managed by this class. So, do not set journal_mode
- * using "PRAGMA journal_mode'<value>" statement if your app is using
- * {@link #enableWriteAheadLogging()}
- * </p>
- *
- * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
- * not supported.
- * @throws SQLException if the SQL string is invalid
- * @see #query(SupportSQLiteQuery)
- */
- void execSQL(String sql) throws SQLException;
-
- /**
- * Execute a single SQL statement that does not return any data.
- * <p>
- * When using {@link #enableWriteAheadLogging()}, journal_mode is
- * automatically managed by this class. So, do not set journal_mode
- * using "PRAGMA journal_mode'<value>" statement if your app is using
- * {@link #enableWriteAheadLogging()}
- * </p>
- *
- * @param sql the SQL statement to be executed. Multiple statements separated by semicolons
- * are
- * not supported.
- * @param bindArgs only byte[], String, Long and Double are supported in selectionArgs.
- * @throws SQLException if the SQL string is invalid
- * @see #query(SupportSQLiteQuery)
- */
- void execSQL(String sql, Object[] bindArgs) throws SQLException;
-
- /**
- * Returns true if the database is opened as read only.
- *
- * @return True if database is opened as read only.
- */
- boolean isReadOnly();
-
- /**
- * Returns true if the database is currently open.
- *
- * @return True if the database is currently open (has not been closed).
- */
- boolean isOpen();
-
- /**
- * Returns true if the new version code is greater than the current database version.
- *
- * @param newVersion The new version code.
- * @return True if the new version code is greater than the current database version.
- */
- boolean needUpgrade(int newVersion);
-
- /**
- * Gets the path to the database file.
- *
- * @return The path to the database file.
- */
- String getPath();
-
- /**
- * Sets the locale for this database. Does nothing if this database has
- * the {@link SQLiteDatabase#NO_LOCALIZED_COLLATORS} flag set or was opened read only.
- *
- * @param locale The new locale.
- * @throws SQLException if the locale could not be set. The most common reason
- * for this is that there is no collator available for the locale you
- * requested.
- * In this case the database remains unchanged.
- */
- void setLocale(Locale locale);
-
- /**
- * Sets the maximum size of the prepared-statement cache for this database.
- * (size of the cache = number of compiled-sql-statements stored in the cache).
- * <p>
- * Maximum cache size can ONLY be increased from its current size (default = 10).
- * If this method is called with smaller size than the current maximum value,
- * then IllegalStateException is thrown.
- * <p>
- * This method is thread-safe.
- *
- * @param cacheSize the size of the cache. can be (0 to
- * {@link SQLiteDatabase#MAX_SQL_CACHE_SIZE})
- * @throws IllegalStateException if input cacheSize gt;
- * {@link SQLiteDatabase#MAX_SQL_CACHE_SIZE}.
- */
- void setMaxSqlCacheSize(int cacheSize);
-
- /**
- * Sets whether foreign key constraints are enabled for the database.
- * <p>
- * By default, foreign key constraints are not enforced by the database.
- * This method allows an application to enable foreign key constraints.
- * It must be called each time the database is opened to ensure that foreign
- * key constraints are enabled for the session.
- * </p><p>
- * A good time to call this method is right after calling {@code #openOrCreateDatabase}
- * or in the {@link SupportSQLiteOpenHelper.Callback#onConfigure} callback.
- * </p><p>
- * When foreign key constraints are disabled, the database does not check whether
- * changes to the database will violate foreign key constraints. Likewise, when
- * foreign key constraints are disabled, the database will not execute cascade
- * delete or update triggers. As a result, it is possible for the database
- * state to become inconsistent. To perform a database integrity check,
- * call {@link #isDatabaseIntegrityOk}.
- * </p><p>
- * This method must not be called while a transaction is in progress.
- * </p><p>
- * See also <a href="http://sqlite.org/foreignkeys.html">SQLite Foreign Key Constraints</a>
- * for more details about foreign key constraint support.
- * </p>
- *
- * @param enable True to enable foreign key constraints, false to disable them.
- * @throws IllegalStateException if the are transactions is in progress
- * when this method is called.
- */
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- void setForeignKeyConstraintsEnabled(boolean enable);
-
- /**
- * This method enables parallel execution of queries from multiple threads on the
- * same database. It does this by opening multiple connections to the database
- * and using a different database connection for each query. The database
- * journal mode is also changed to enable writes to proceed concurrently with reads.
- * <p>
- * When write-ahead logging is not enabled (the default), it is not possible for
- * reads and writes to occur on the database at the same time. Before modifying the
- * database, the writer implicitly acquires an exclusive lock on the database which
- * prevents readers from accessing the database until the write is completed.
- * </p><p>
- * In contrast, when write-ahead logging is enabled (by calling this method), write
- * operations occur in a separate log file which allows reads to proceed concurrently.
- * While a write is in progress, readers on other threads will perceive the state
- * of the database as it was before the write began. When the write completes, readers
- * on other threads will then perceive the new state of the database.
- * </p><p>
- * It is a good idea to enable write-ahead logging whenever a database will be
- * concurrently accessed and modified by multiple threads at the same time.
- * However, write-ahead logging uses significantly more memory than ordinary
- * journaling because there are multiple connections to the same database.
- * So if a database will only be used by a single thread, or if optimizing
- * concurrency is not very important, then write-ahead logging should be disabled.
- * </p><p>
- * After calling this method, execution of queries in parallel is enabled as long as
- * the database remains open. To disable execution of queries in parallel, either
- * call {@link #disableWriteAheadLogging} or close the database and reopen it.
- * </p><p>
- * The maximum number of connections used to execute queries in parallel is
- * dependent upon the device memory and possibly other properties.
- * </p><p>
- * If a query is part of a transaction, then it is executed on the same database handle the
- * transaction was begun.
- * </p><p>
- * Writers should use {@link #beginTransactionNonExclusive()} or
- * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)}
- * to start a transaction. Non-exclusive mode allows database file to be in readable
- * by other threads executing queries.
- * </p><p>
- * If the database has any attached databases, then execution of queries in parallel is NOT
- * possible. Likewise, write-ahead logging is not supported for read-only databases
- * or memory databases. In such cases, {@code enableWriteAheadLogging} returns false.
- * </p><p>
- * The best way to enable write-ahead logging is to pass the
- * {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag to
- * {@link SQLiteDatabase#openDatabase}. This is more efficient than calling
- * <code><pre>
- * SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
- * SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING,
- * myDatabaseErrorHandler);
- * db.enableWriteAheadLogging();
- * </pre></code>
- * </p><p>
- * Another way to enable write-ahead logging is to call {@code enableWriteAheadLogging}
- * after opening the database.
- * <code><pre>
- * SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
- * SQLiteDatabase.CREATE_IF_NECESSARY, myDatabaseErrorHandler);
- * db.enableWriteAheadLogging();
- * </pre></code>
- * </p><p>
- * See also <a href="http://sqlite.org/wal.html">SQLite Write-Ahead Logging</a> for
- * more details about how write-ahead logging works.
- * </p>
- *
- * @return True if write-ahead logging is enabled.
- * @throws IllegalStateException if there are transactions in progress at the
- * time this method is called. WAL mode can only be changed when
- * there are no
- * transactions in progress.
- * @see SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING
- * @see #disableWriteAheadLogging
- */
- boolean enableWriteAheadLogging();
-
- /**
- * This method disables the features enabled by {@link #enableWriteAheadLogging()}.
- *
- * @throws IllegalStateException if there are transactions in progress at the
- * time this method is called. WAL mode can only be changed when
- * there are no
- * transactions in progress.
- * @see #enableWriteAheadLogging
- */
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- void disableWriteAheadLogging();
-
- /**
- * Returns true if write-ahead logging has been enabled for this database.
- *
- * @return True if write-ahead logging has been enabled for this database.
- * @see #enableWriteAheadLogging
- * @see SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING
- */
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- boolean isWriteAheadLoggingEnabled();
-
- /**
- * Returns list of full path names of all attached databases including the main database
- * by executing 'pragma database_list' on the database.
- *
- * @return ArrayList of pairs of (database name, database file path) or null if the database
- * is not open.
- */
- List<Pair<String, String>> getAttachedDbs();
-
- /**
- * Runs 'pragma integrity_check' on the given database (and all the attached databases)
- * and returns true if the given database (and all its attached databases) pass integrity_check,
- * false otherwise.
- * <p>
- * If the result is false, then this method logs the errors reported by the integrity_check
- * command execution.
- * <p>
- * Note that 'pragma integrity_check' on a database can take a long time.
- *
- * @return true if the given database (and all its attached databases) pass integrity_check,
- * false otherwise.
- */
- boolean isDatabaseIntegrityOk();
-}
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteOpenHelper.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteOpenHelper.java
deleted file mode 100644
index 02e4e7d..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteOpenHelper.java
+++ /dev/null
@@ -1,391 +0,0 @@
-/*
- * Copyright (C) 2016 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.db;
-
-import android.content.Context;
-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}.
- * Note that since that class requires overriding certain methods, support implementation
- * uses {@link Factory#create(Configuration)} to create this and {@link Callback} to implement
- * the methods that should be overridden.
- */
-@SuppressWarnings("unused")
-public interface SupportSQLiteOpenHelper {
- /**
- * Return the name of the SQLite database being opened, as given to
- * the constructor.
- */
- String getDatabaseName();
-
- /**
- * Enables or disables the use of write-ahead logging for the database.
- *
- * Write-ahead logging cannot be used with read-only databases so the value of
- * this flag is ignored if the database is opened read-only.
- *
- * @param enabled True if write-ahead logging should be enabled, false if it
- * should be disabled.
- * @see SupportSQLiteDatabase#enableWriteAheadLogging()
- */
- @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- void setWriteAheadLoggingEnabled(boolean enabled);
-
- /**
- * Create and/or open a database that will be used for reading and writing.
- * The first time this is called, the database will be opened and
- * {@link Callback#onCreate}, {@link Callback#onUpgrade} and/or {@link Callback#onOpen} will be
- * called.
- *
- * <p>Once opened successfully, the database is cached, so you can
- * call this method every time you need to write to the database.
- * (Make sure to call {@link #close} when you no longer need the database.)
- * Errors such as bad permissions or a full disk may cause this method
- * to fail, but future attempts may succeed if the problem is fixed.</p>
- *
- * <p class="caution">Database upgrade may take a long time, you
- * should not call this method from the application main thread, including
- * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
- *
- * @return a read/write database object valid until {@link #close} is called
- * @throws SQLiteException if the database cannot be opened for writing
- */
- SupportSQLiteDatabase getWritableDatabase();
-
- /**
- * Create and/or open a database. This will be the same object returned by
- * {@link #getWritableDatabase} unless some problem, such as a full disk,
- * requires the database to be opened read-only. In that case, a read-only
- * database object will be returned. If the problem is fixed, a future call
- * to {@link #getWritableDatabase} may succeed, in which case the read-only
- * database object will be closed and the read/write object will be returned
- * in the future.
- *
- * <p class="caution">Like {@link #getWritableDatabase}, this method may
- * take a long time to return, so you should not call it from the
- * application main thread, including from
- * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
- *
- * @return a database object valid until {@link #getWritableDatabase}
- * or {@link #close} is called.
- * @throws SQLiteException if the database cannot be opened
- */
- SupportSQLiteDatabase getReadableDatabase();
-
- /**
- * Close any open database object.
- */
- void close();
-
- /**
- * 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.
- * <p>
- * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade},
- * or {@link #onOpen} are called. It should not modify the database except to configure the
- * database connection as required.
- * </p>
- * <p>
- * This method should only call methods that configure the parameters of the database
- * connection, such as {@link SupportSQLiteDatabase#enableWriteAheadLogging}
- * {@link SupportSQLiteDatabase#setForeignKeyConstraintsEnabled},
- * {@link SupportSQLiteDatabase#setLocale},
- * {@link SupportSQLiteDatabase#setMaximumSize}, or executing PRAGMA statements.
- * </p>
- *
- * @param db The database.
- */
- public void onConfigure(SupportSQLiteDatabase db) {
-
- }
-
- /**
- * Called when the database is created for the first time. This is where the
- * creation of tables and the initial population of the tables should happen.
- *
- * @param db The database.
- */
- public abstract void onCreate(SupportSQLiteDatabase db);
-
- /**
- * Called when the database needs to be upgraded. The implementation
- * should use this method to drop tables, add tables, or do anything else it
- * needs to upgrade to the new schema version.
- *
- * <p>
- * The SQLite ALTER TABLE documentation can be found
- * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
- * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
- * you can use ALTER TABLE to rename the old table, then create the new table and then
- * populate the new table with the contents of the old table.
- * </p><p>
- * This method executes within a transaction. If an exception is thrown, all changes
- * will automatically be rolled back.
- * </p>
- *
- * @param db The database.
- * @param oldVersion The old database version.
- * @param newVersion The new database version.
- */
- public abstract void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion);
-
- /**
- * Called when the database needs to be downgraded. This is strictly similar to
- * {@link #onUpgrade} method, but is called whenever current version is newer than requested
- * one.
- * However, this method is not abstract, so it is not mandatory for a customer to
- * implement it. If not overridden, default implementation will reject downgrade and
- * throws SQLiteException
- *
- * <p>
- * This method executes within a transaction. If an exception is thrown, all changes
- * will automatically be rolled back.
- * </p>
- *
- * @param db The database.
- * @param oldVersion The old database version.
- * @param newVersion The new database version.
- */
- public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
- throw new SQLiteException("Can't downgrade database from version "
- + oldVersion + " to " + newVersion);
- }
-
- /**
- * Called when the database has been opened. The implementation
- * should check {@link SupportSQLiteDatabase#isReadOnly} before updating the
- * database.
- * <p>
- * This method is called after the database connection has been configured
- * and after the database schema has been created, upgraded or downgraded as necessary.
- * If the database connection must be configured in some way before the schema
- * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead.
- * </p>
- *
- * @param db The database.
- */
- 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);
- }
- }
- }
-
- /**
- * The configuration to create an SQLite open helper object using {@link Factory}.
- */
- @SuppressWarnings("WeakerAccess")
- class Configuration {
- /**
- * Context to use to open or create the database.
- */
- @NonNull
- public final Context context;
- /**
- * Name of the database file, or null for an in-memory database.
- */
- @Nullable
- public final String name;
- /**
- * The callback class to handle creation, upgrade and downgrade.
- */
- @NonNull
- public final SupportSQLiteOpenHelper.Callback callback;
-
- Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) {
- this.context = context;
- this.name = name;
- this.callback = callback;
- }
-
- /**
- * Creates a new Configuration.Builder to create an instance of Configuration.
- *
- * @param context to use to open or create the database.
- */
- public static Builder builder(Context context) {
- return new Builder(context);
- }
-
- /**
- * Builder class for {@link Configuration}.
- */
- public static class Builder {
- Context mContext;
- String mName;
- SupportSQLiteOpenHelper.Callback mCallback;
-
- public Configuration build() {
- if (mCallback == null) {
- throw new IllegalArgumentException("Must set a callback to create the"
- + " configuration.");
- }
- if (mContext == null) {
- throw new IllegalArgumentException("Must set a non-null context to create"
- + " the configuration.");
- }
- return new Configuration(mContext, mName, mCallback);
- }
-
- Builder(@NonNull Context context) {
- mContext = context;
- }
-
- /**
- * @param name Name of the database file, or null for an in-memory database.
- * @return This
- */
- public Builder name(@Nullable String name) {
- mName = name;
- return this;
- }
-
- /**
- * @param callback The callback class to handle creation, upgrade and downgrade.
- * @return this
- */
- public Builder callback(@NonNull Callback callback) {
- mCallback = callback;
- return this;
- }
- }
- }
-
- /**
- * Factory class to create instances of {@link SupportSQLiteOpenHelper} using
- * {@link Configuration}.
- */
- interface Factory {
- /**
- * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration.
- *
- * @param configuration The configuration to use while creating the open helper.
- *
- * @return A SupportSQLiteOpenHelper which can be used to open a database.
- */
- SupportSQLiteOpenHelper create(Configuration configuration);
- }
-}
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteProgram.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteProgram.java
deleted file mode 100644
index 38c1ac1..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteProgram.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2016 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.db;
-
-import java.io.Closeable;
-
-/**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteProgram}.
- */
-
-@SuppressWarnings("unused")
-public interface SupportSQLiteProgram extends Closeable {
- /**
- * Bind a NULL value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
- *
- * @param index The 1-based index to the parameter to bind null to
- */
- void bindNull(int index);
-
- /**
- * Bind a long value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
- *addToBindArgs
- * @param index The 1-based index to the parameter to bind
- * @param value The value to bind
- */
- void bindLong(int index, long value);
-
- /**
- * Bind a double value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
- *
- * @param index The 1-based index to the parameter to bind
- * @param value The value to bind
- */
- void bindDouble(int index, double value);
-
- /**
- * Bind a String value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
- *
- * @param index The 1-based index to the parameter to bind
- * @param value The value to bind, must not be null
- */
- void bindString(int index, String value);
-
- /**
- * Bind a byte array value to this statement. The value remains bound until
- * {@link #clearBindings} is called.
- *
- * @param index The 1-based index to the parameter to bind
- * @param value The value to bind, must not be null
- */
- void bindBlob(int index, byte[] value);
-
- /**
- * Clears all existing bindings. Unset bindings are treated as NULL.
- */
- void clearBindings();
-}
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java
deleted file mode 100644
index 03e0a91..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.db;
-
-/**
- * A query with typed bindings. It is better to use this API instead of
- * {@link android.database.sqlite.SQLiteDatabase#rawQuery(String, String[])} because it allows
- * binding type safe parameters.
- */
-public interface SupportSQLiteQuery {
- /**
- * The SQL query. This query can have placeholders(?) for bind arguments.
- *
- * @return The SQL query to compile
- */
- String getSql();
-
- /**
- * Callback to bind the query parameters to the compiled statement.
- *
- * @param statement The compiled statement
- */
- void bindTo(SupportSQLiteProgram statement);
-
- /**
- * Returns the number of arguments in this query. This is equal to the number of placeholders
- * in the query string. See: https://www.sqlite.org/c3ref/bind_blob.html for details.
- *
- * @return The number of arguments in the query.
- */
- int getArgCount();
-}
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQueryBuilder.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQueryBuilder.java
deleted file mode 100644
index 52957a3..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQueryBuilder.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * 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.db;
-
-import java.util.regex.Pattern;
-
-/**
- * A simple query builder to create SQL SELECT queries.
- */
-@SuppressWarnings("unused")
-public final class SupportSQLiteQueryBuilder {
- private static final Pattern sLimitPattern =
- Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
-
- private boolean mDistinct = false;
- private final String mTable;
- private String[] mColumns = null;
- private String mSelection;
- private Object[] mBindArgs;
- private String mGroupBy = null;
- private String mHaving = null;
- private String mOrderBy = null;
- private String mLimit = null;
-
- /**
- * Creates a query for the given table name.
- *
- * @param tableName The table name(s) to query.
- *
- * @return A builder to create a query.
- */
- public static SupportSQLiteQueryBuilder builder(String tableName) {
- return new SupportSQLiteQueryBuilder(tableName);
- }
-
- private SupportSQLiteQueryBuilder(String table) {
- mTable = table;
- }
-
- /**
- * Adds DISTINCT keyword to the query.
- *
- * @return this
- */
- public SupportSQLiteQueryBuilder distinct() {
- mDistinct = true;
- return this;
- }
-
- /**
- * Sets the given list of columns as the columns that will be returned.
- *
- * @param columns The list of column names that should be returned.
- *
- * @return this
- */
- public SupportSQLiteQueryBuilder columns(String[] columns) {
- mColumns = columns;
- return this;
- }
-
- /**
- * Sets the arguments for the WHERE clause.
- *
- * @param selection The list of selection columns
- * @param bindArgs The list of bind arguments to match against these columns
- *
- * @return this
- */
- public SupportSQLiteQueryBuilder selection(String selection, Object[] bindArgs) {
- mSelection = selection;
- mBindArgs = bindArgs;
- return this;
- }
-
- /**
- * Adds a GROUP BY statement.
- *
- * @param groupBy The value of the GROUP BY statement.
- *
- * @return this
- */
- @SuppressWarnings("WeakerAccess")
- public SupportSQLiteQueryBuilder groupBy(String groupBy) {
- mGroupBy = groupBy;
- return this;
- }
-
- /**
- * Adds a HAVING statement. You must also provide {@link #groupBy(String)} for this to work.
- *
- * @param having The having clause.
- *
- * @return this
- */
- public SupportSQLiteQueryBuilder having(String having) {
- mHaving = having;
- return this;
- }
-
- /**
- * Adds an ORDER BY statement.
- *
- * @param orderBy The order clause.
- *
- * @return this
- */
- public SupportSQLiteQueryBuilder orderBy(String orderBy) {
- mOrderBy = orderBy;
- return this;
- }
-
- /**
- * Adds a LIMIT statement.
- *
- * @param limit The limit value.
- *
- * @return this
- */
- public SupportSQLiteQueryBuilder limit(String limit) {
- if (!isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
- throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
- }
- mLimit = limit;
- return this;
- }
-
- /**
- * Creates the {@link SupportSQLiteQuery} that can be passed into
- * {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
- *
- * @return a new query
- */
- public SupportSQLiteQuery create() {
- if (isEmpty(mGroupBy) && !isEmpty(mHaving)) {
- throw new IllegalArgumentException(
- "HAVING clauses are only permitted when using a groupBy clause");
- }
- StringBuilder query = new StringBuilder(120);
-
- query.append("SELECT ");
- if (mDistinct) {
- query.append("DISTINCT ");
- }
- if (mColumns != null && mColumns.length != 0) {
- appendColumns(query, mColumns);
- } else {
- query.append(" * ");
- }
- query.append(" FROM ");
- query.append(mTable);
- appendClause(query, " WHERE ", mSelection);
- appendClause(query, " GROUP BY ", mGroupBy);
- appendClause(query, " HAVING ", mHaving);
- appendClause(query, " ORDER BY ", mOrderBy);
- appendClause(query, " LIMIT ", mLimit);
-
- return new SimpleSQLiteQuery(query.toString(), mBindArgs);
- }
-
- private static void appendClause(StringBuilder s, String name, String clause) {
- if (!isEmpty(clause)) {
- s.append(name);
- s.append(clause);
- }
- }
-
- /**
- * Add the names that are non-null in columns to s, separating
- * them with commas.
- */
- private static void appendColumns(StringBuilder s, String[] columns) {
- int n = columns.length;
-
- for (int i = 0; i < n; i++) {
- String column = columns[i];
- if (i > 0) {
- s.append(", ");
- }
- s.append(column);
- }
- s.append(' ');
- }
-
- private static boolean isEmpty(String input) {
- return input == null || input.length() == 0;
- }
-}
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteStatement.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteStatement.java
deleted file mode 100644
index 5a329d7..0000000
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteStatement.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2016 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.db;
-
-/**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteStatement}.
- */
-@SuppressWarnings("unused")
-public interface SupportSQLiteStatement extends SupportSQLiteProgram {
- /**
- * Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example
- * CREATE / DROP table, view, trigger, index etc.
- *
- * @throws android.database.SQLException If the SQL string is invalid for
- * some reason
- */
- void execute();
-
- /**
- * Execute this SQL statement, if the the number of rows affected by execution of this SQL
- * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
- *
- * @return the number of rows affected by this SQL statement execution.
- * @throws android.database.SQLException If the SQL string is invalid for
- * some reason
- */
- int executeUpdateDelete();
-
- /**
- * Execute this SQL statement and return the ID of the row inserted due to this call.
- * The SQL statement should be an INSERT for this to be a useful call.
- *
- * @return the row ID of the last row inserted, if this insert is successful. -1 otherwise.
- *
- * @throws android.database.SQLException If the SQL string is invalid for
- * some reason
- */
- long executeInsert();
-
- /**
- * Execute a statement that returns a 1 by 1 table with a numeric value.
- * For example, SELECT COUNT(*) FROM table;
- *
- * @return The result of the query.
- *
- * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
- */
- long simpleQueryForLong();
- /**
- * Execute a statement that returns a 1 by 1 table with a text value.
- * For example, SELECT COUNT(*) FROM table;
- *
- * @return The result of the query.
- *
- * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
- */
- String simpleQueryForString();
-}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java b/persistence/db/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java
new file mode 100644
index 0000000..bdcbb9f
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.java
@@ -0,0 +1,107 @@
+/*
+ * 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 androidx.sqlite.db;
+
+/**
+ * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
+ * binds args based on the passed in Object type.
+ */
+public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
+ private final String mQuery;
+ private final Object[] mBindArgs;
+
+ /**
+ * Creates an SQL query with the sql string and the bind arguments.
+ *
+ * @param query The query string, can include bind arguments (.e.g ?).
+ * @param bindArgs The bind argument value that will replace the placeholders in the query.
+ */
+ public SimpleSQLiteQuery(String query, Object[] bindArgs) {
+ mQuery = query;
+ mBindArgs = bindArgs;
+ }
+
+ /**
+ * Creates an SQL query without any bind arguments.
+ *
+ * @param query The SQL query to execute. Cannot include bind parameters.
+ */
+ public SimpleSQLiteQuery(String query) {
+ this(query, null);
+ }
+
+ @Override
+ public String getSql() {
+ return mQuery;
+ }
+
+ @Override
+ public void bindTo(SupportSQLiteProgram statement) {
+ bind(statement, mBindArgs);
+ }
+
+ @Override
+ public int getArgCount() {
+ return mBindArgs.length;
+ }
+
+ /**
+ * Binds the given arguments into the given sqlite statement.
+ *
+ * @param statement The sqlite statement
+ * @param bindArgs The list of bind arguments
+ */
+ public static void bind(SupportSQLiteProgram statement, Object[] bindArgs) {
+ if (bindArgs == null) {
+ return;
+ }
+ final int limit = bindArgs.length;
+ for (int i = 0; i < limit; i++) {
+ final Object arg = bindArgs[i];
+ bind(statement, i + 1, arg);
+ }
+ }
+
+ private static void bind(SupportSQLiteProgram statement, int index, Object arg) {
+ // extracted from android.database.sqlite.SQLiteConnection
+ if (arg == null) {
+ statement.bindNull(index);
+ } else if (arg instanceof byte[]) {
+ statement.bindBlob(index, (byte[]) arg);
+ } else if (arg instanceof Float) {
+ statement.bindDouble(index, (Float) arg);
+ } else if (arg instanceof Double) {
+ statement.bindDouble(index, (Double) arg);
+ } else if (arg instanceof Long) {
+ statement.bindLong(index, (Long) arg);
+ } else if (arg instanceof Integer) {
+ statement.bindLong(index, (Integer) arg);
+ } else if (arg instanceof Short) {
+ statement.bindLong(index, (Short) arg);
+ } else if (arg instanceof Byte) {
+ statement.bindLong(index, (Byte) arg);
+ } else if (arg instanceof String) {
+ statement.bindString(index, (String) arg);
+ } else if (arg instanceof Boolean) {
+ statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
+ } else {
+ throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
+ + " Supported types: null, byte[], float, double, long, int, short, byte,"
+ + " string");
+ }
+ }
+}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.java b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.java
new file mode 100644
index 0000000..961944b
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.Closeable;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A database abstraction which removes the framework dependency and allows swapping underlying
+ * sql versions. It mimics the behavior of {@link android.database.sqlite.SQLiteDatabase}
+ */
+@SuppressWarnings("unused")
+public interface SupportSQLiteDatabase extends Closeable {
+ /**
+ * Compiles the given SQL statement.
+ *
+ * @param sql The sql query.
+ * @return Compiled statement.
+ */
+ SupportSQLiteStatement compileStatement(String sql);
+
+ /**
+ * Begins a transaction in EXCLUSIVE mode.
+ * <p>
+ * Transactions can be nested.
+ * When the outer transaction is ended all of
+ * the work done in that transaction and all of the nested transactions will be committed or
+ * rolled back. The changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
+ * </p>
+ * <p>Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransaction();
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ */
+ void beginTransaction();
+
+ /**
+ * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When
+ * the outer transaction is ended all of the work done in that transaction
+ * and all of the nested transactions will be committed or rolled back. The
+ * changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they
+ * will be committed.
+ * <p>
+ * Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransactionNonExclusive();
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ */
+ void beginTransactionNonExclusive();
+
+ /**
+ * Begins a transaction in EXCLUSIVE mode.
+ * <p>
+ * Transactions can be nested.
+ * When the outer transaction is ended all of
+ * the work done in that transaction and all of the nested transactions will be committed or
+ * rolled back. The changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
+ * </p>
+ * <p>Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransactionWithListener(listener);
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ *
+ * @param transactionListener listener that should be notified when the transaction begins,
+ * commits, or is rolled back, either explicitly or by a call to
+ * {@link #yieldIfContendedSafely}.
+ */
+ void beginTransactionWithListener(SQLiteTransactionListener transactionListener);
+
+ /**
+ * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When
+ * the outer transaction is ended all of the work done in that transaction
+ * and all of the nested transactions will be committed or rolled back. The
+ * changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they
+ * will be committed.
+ * <p>
+ * Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransactionWithListenerNonExclusive(listener);
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ *
+ * @param transactionListener listener that should be notified when the
+ * transaction begins, commits, or is rolled back, either
+ * explicitly or by a call to {@link #yieldIfContendedSafely}.
+ */
+ void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener);
+
+ /**
+ * End a transaction. See beginTransaction for notes about how to use this and when transactions
+ * are committed and rolled back.
+ */
+ void endTransaction();
+
+ /**
+ * Marks the current transaction as successful. Do not do any more database work between
+ * calling this and calling endTransaction. Do as little non-database work as possible in that
+ * situation too. If any errors are encountered between this and endTransaction the transaction
+ * will still be committed.
+ *
+ * @throws IllegalStateException if the current thread is not in a transaction or the
+ * transaction is already marked as successful.
+ */
+ void setTransactionSuccessful();
+
+ /**
+ * Returns true if the current thread has a transaction pending.
+ *
+ * @return True if the current thread is in a transaction.
+ */
+ boolean inTransaction();
+
+ /**
+ * Returns true if the current thread is holding an active connection to the database.
+ * <p>
+ * The name of this method comes from a time when having an active connection
+ * to the database meant that the thread was holding an actual lock on the
+ * database. Nowadays, there is no longer a true "database lock" although threads
+ * may block if they cannot acquire a database connection to perform a
+ * particular operation.
+ * </p>
+ *
+ * @return True if the current thread is holding an active connection to the database.
+ */
+ boolean isDbLockedByCurrentThread();
+
+ /**
+ * Temporarily end the transaction to let other threads run. The transaction is assumed to be
+ * successful so far. Do not call setTransactionSuccessful before calling this. When this
+ * returns a new transaction will have been created but not marked as successful. This assumes
+ * that there are no nested transactions (beginTransaction has only been called once) and will
+ * throw an exception if that is not the case.
+ *
+ * @return true if the transaction was yielded
+ */
+ boolean yieldIfContendedSafely();
+
+ /**
+ * Temporarily end the transaction to let other threads run. The transaction is assumed to be
+ * successful so far. Do not call setTransactionSuccessful before calling this. When this
+ * returns a new transaction will have been created but not marked as successful. This assumes
+ * that there are no nested transactions (beginTransaction has only been called once) and will
+ * throw an exception if that is not the case.
+ *
+ * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if
+ * the lock was actually yielded. This will allow other background
+ * threads to make some
+ * more progress than they would if we started the transaction
+ * immediately.
+ * @return true if the transaction was yielded
+ */
+ boolean yieldIfContendedSafely(long sleepAfterYieldDelay);
+
+ /**
+ * Gets the database version.
+ *
+ * @return the database version
+ */
+ int getVersion();
+
+ /**
+ * Sets the database version.
+ *
+ * @param version the new database version
+ */
+ void setVersion(int version);
+
+ /**
+ * Returns the maximum size the database may grow to.
+ *
+ * @return the new maximum database size
+ */
+ long getMaximumSize();
+
+ /**
+ * Sets the maximum size the database will grow to. The maximum size cannot
+ * be set below the current size.
+ *
+ * @param numBytes the maximum database size, in bytes
+ * @return the new maximum database size
+ */
+ long setMaximumSize(long numBytes);
+
+ /**
+ * Returns the current database page size, in bytes.
+ *
+ * @return the database page size, in bytes
+ */
+ long getPageSize();
+
+ /**
+ * Sets the database page size. The page size must be a power of two. This
+ * method does not work if any data has been written to the database file,
+ * and must be called right after the database has been created.
+ *
+ * @param numBytes the database page size, in bytes
+ */
+ void setPageSize(long numBytes);
+
+ /**
+ * Runs the given query on the database. If you would like to have typed bind arguments,
+ * use {@link #query(SupportSQLiteQuery)}.
+ *
+ * @param query The SQL query that includes the query and can bind into a given compiled
+ * program.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ * @see #query(SupportSQLiteQuery)
+ */
+ Cursor query(String query);
+
+ /**
+ * Runs the given query on the database. If you would like to have bind arguments,
+ * use {@link #query(SupportSQLiteQuery)}.
+ *
+ * @param query The SQL query that includes the query and can bind into a given compiled
+ * program.
+ * @param bindArgs The query arguments to bind.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ * @see #query(SupportSQLiteQuery)
+ */
+ Cursor query(String query, Object[] bindArgs);
+
+ /**
+ * Runs the given query on the database.
+ * <p>
+ * This class allows using type safe sql program bindings while running queries.
+ *
+ * @param query The SQL query that includes the query and can bind into a given compiled
+ * program.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ * @see SimpleSQLiteQuery
+ */
+ Cursor query(SupportSQLiteQuery query);
+
+ /**
+ * Runs the given query on the database.
+ * <p>
+ * This class allows using type safe sql program bindings while running queries.
+ *
+ * @param query The SQL query that includes the query and can bind into a given compiled
+ * program.
+ * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A {@link Cursor} object, which is positioned before the first entry. Note that
+ * {@link Cursor}s are not synchronized, see the documentation for more details.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal);
+
+ /**
+ * Convenience method for inserting a row into the database.
+ *
+ * @param table the table to insert the row into
+ * @param values this map contains the initial column values for the
+ * row. The keys should be the column names and the values the
+ * column values
+ * @param conflictAlgorithm for insert conflict resolver. One of
+ * {@link SQLiteDatabase#CONFLICT_NONE}, {@link SQLiteDatabase#CONFLICT_ROLLBACK},
+ * {@link SQLiteDatabase#CONFLICT_ABORT}, {@link SQLiteDatabase#CONFLICT_FAIL},
+ * {@link SQLiteDatabase#CONFLICT_IGNORE}, {@link SQLiteDatabase#CONFLICT_REPLACE}.
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ * @throws SQLException If the insert fails
+ */
+ long insert(String table, int conflictAlgorithm, ContentValues values) throws SQLException;
+
+ /**
+ * Convenience method for deleting rows in the database.
+ *
+ * @param table the table to delete from
+ * @param whereClause the optional WHERE clause to apply when deleting.
+ * Passing null will delete all rows.
+ * @param whereArgs You may include ?s in the where clause, which
+ * will be replaced by the values from whereArgs. The values
+ * will be bound as Strings.
+ * @return the number of rows affected if a whereClause is passed in, 0
+ * otherwise. To remove all rows and get a count pass "1" as the
+ * whereClause.
+ */
+ int delete(String table, String whereClause, Object[] whereArgs);
+
+ /**
+ * Convenience method for updating rows in the database.
+ *
+ * @param table the table to update in
+ * @param conflictAlgorithm for update conflict resolver. One of
+ * {@link SQLiteDatabase#CONFLICT_NONE}, {@link SQLiteDatabase#CONFLICT_ROLLBACK},
+ * {@link SQLiteDatabase#CONFLICT_ABORT}, {@link SQLiteDatabase#CONFLICT_FAIL},
+ * {@link SQLiteDatabase#CONFLICT_IGNORE}, {@link SQLiteDatabase#CONFLICT_REPLACE}.
+ * @param values a map from column names to new column values. null is a
+ * valid value that will be translated to NULL.
+ * @param whereClause the optional WHERE clause to apply when updating.
+ * Passing null will update all rows.
+ * @param whereArgs You may include ?s in the where clause, which
+ * will be replaced by the values from whereArgs. The values
+ * will be bound as Strings.
+ * @return the number of rows affected
+ */
+ int update(String table, int conflictAlgorithm,
+ ContentValues values, String whereClause, Object[] whereArgs);
+
+ /**
+ * Execute a single SQL statement that does not return any data.
+ * <p>
+ * When using {@link #enableWriteAheadLogging()}, journal_mode is
+ * automatically managed by this class. So, do not set journal_mode
+ * using "PRAGMA journal_mode'<value>" statement if your app is using
+ * {@link #enableWriteAheadLogging()}
+ * </p>
+ *
+ * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are
+ * not supported.
+ * @throws SQLException if the SQL string is invalid
+ * @see #query(SupportSQLiteQuery)
+ */
+ void execSQL(String sql) throws SQLException;
+
+ /**
+ * Execute a single SQL statement that does not return any data.
+ * <p>
+ * When using {@link #enableWriteAheadLogging()}, journal_mode is
+ * automatically managed by this class. So, do not set journal_mode
+ * using "PRAGMA journal_mode'<value>" statement if your app is using
+ * {@link #enableWriteAheadLogging()}
+ * </p>
+ *
+ * @param sql the SQL statement to be executed. Multiple statements separated by semicolons
+ * are
+ * not supported.
+ * @param bindArgs only byte[], String, Long and Double are supported in selectionArgs.
+ * @throws SQLException if the SQL string is invalid
+ * @see #query(SupportSQLiteQuery)
+ */
+ void execSQL(String sql, Object[] bindArgs) throws SQLException;
+
+ /**
+ * Returns true if the database is opened as read only.
+ *
+ * @return True if database is opened as read only.
+ */
+ boolean isReadOnly();
+
+ /**
+ * Returns true if the database is currently open.
+ *
+ * @return True if the database is currently open (has not been closed).
+ */
+ boolean isOpen();
+
+ /**
+ * Returns true if the new version code is greater than the current database version.
+ *
+ * @param newVersion The new version code.
+ * @return True if the new version code is greater than the current database version.
+ */
+ boolean needUpgrade(int newVersion);
+
+ /**
+ * Gets the path to the database file.
+ *
+ * @return The path to the database file.
+ */
+ String getPath();
+
+ /**
+ * Sets the locale for this database. Does nothing if this database has
+ * the {@link SQLiteDatabase#NO_LOCALIZED_COLLATORS} flag set or was opened read only.
+ *
+ * @param locale The new locale.
+ * @throws SQLException if the locale could not be set. The most common reason
+ * for this is that there is no collator available for the locale you
+ * requested.
+ * In this case the database remains unchanged.
+ */
+ void setLocale(Locale locale);
+
+ /**
+ * Sets the maximum size of the prepared-statement cache for this database.
+ * (size of the cache = number of compiled-sql-statements stored in the cache).
+ * <p>
+ * Maximum cache size can ONLY be increased from its current size (default = 10).
+ * If this method is called with smaller size than the current maximum value,
+ * then IllegalStateException is thrown.
+ * <p>
+ * This method is thread-safe.
+ *
+ * @param cacheSize the size of the cache. can be (0 to
+ * {@link SQLiteDatabase#MAX_SQL_CACHE_SIZE})
+ * @throws IllegalStateException if input cacheSize gt;
+ * {@link SQLiteDatabase#MAX_SQL_CACHE_SIZE}.
+ */
+ void setMaxSqlCacheSize(int cacheSize);
+
+ /**
+ * Sets whether foreign key constraints are enabled for the database.
+ * <p>
+ * By default, foreign key constraints are not enforced by the database.
+ * This method allows an application to enable foreign key constraints.
+ * It must be called each time the database is opened to ensure that foreign
+ * key constraints are enabled for the session.
+ * </p><p>
+ * A good time to call this method is right after calling {@code #openOrCreateDatabase}
+ * or in the {@link SupportSQLiteOpenHelper.Callback#onConfigure} callback.
+ * </p><p>
+ * When foreign key constraints are disabled, the database does not check whether
+ * changes to the database will violate foreign key constraints. Likewise, when
+ * foreign key constraints are disabled, the database will not execute cascade
+ * delete or update triggers. As a result, it is possible for the database
+ * state to become inconsistent. To perform a database integrity check,
+ * call {@link #isDatabaseIntegrityOk}.
+ * </p><p>
+ * This method must not be called while a transaction is in progress.
+ * </p><p>
+ * See also <a href="http://sqlite.org/foreignkeys.html">SQLite Foreign Key Constraints</a>
+ * for more details about foreign key constraint support.
+ * </p>
+ *
+ * @param enable True to enable foreign key constraints, false to disable them.
+ * @throws IllegalStateException if the are transactions is in progress
+ * when this method is called.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ void setForeignKeyConstraintsEnabled(boolean enable);
+
+ /**
+ * This method enables parallel execution of queries from multiple threads on the
+ * same database. It does this by opening multiple connections to the database
+ * and using a different database connection for each query. The database
+ * journal mode is also changed to enable writes to proceed concurrently with reads.
+ * <p>
+ * When write-ahead logging is not enabled (the default), it is not possible for
+ * reads and writes to occur on the database at the same time. Before modifying the
+ * database, the writer implicitly acquires an exclusive lock on the database which
+ * prevents readers from accessing the database until the write is completed.
+ * </p><p>
+ * In contrast, when write-ahead logging is enabled (by calling this method), write
+ * operations occur in a separate log file which allows reads to proceed concurrently.
+ * While a write is in progress, readers on other threads will perceive the state
+ * of the database as it was before the write began. When the write completes, readers
+ * on other threads will then perceive the new state of the database.
+ * </p><p>
+ * It is a good idea to enable write-ahead logging whenever a database will be
+ * concurrently accessed and modified by multiple threads at the same time.
+ * However, write-ahead logging uses significantly more memory than ordinary
+ * journaling because there are multiple connections to the same database.
+ * So if a database will only be used by a single thread, or if optimizing
+ * concurrency is not very important, then write-ahead logging should be disabled.
+ * </p><p>
+ * After calling this method, execution of queries in parallel is enabled as long as
+ * the database remains open. To disable execution of queries in parallel, either
+ * call {@link #disableWriteAheadLogging} or close the database and reopen it.
+ * </p><p>
+ * The maximum number of connections used to execute queries in parallel is
+ * dependent upon the device memory and possibly other properties.
+ * </p><p>
+ * If a query is part of a transaction, then it is executed on the same database handle the
+ * transaction was begun.
+ * </p><p>
+ * Writers should use {@link #beginTransactionNonExclusive()} or
+ * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)}
+ * to start a transaction. Non-exclusive mode allows database file to be in readable
+ * by other threads executing queries.
+ * </p><p>
+ * If the database has any attached databases, then execution of queries in parallel is NOT
+ * possible. Likewise, write-ahead logging is not supported for read-only databases
+ * or memory databases. In such cases, {@code enableWriteAheadLogging} returns false.
+ * </p><p>
+ * The best way to enable write-ahead logging is to pass the
+ * {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag to
+ * {@link SQLiteDatabase#openDatabase}. This is more efficient than calling
+ * <code><pre>
+ * SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+ * SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING,
+ * myDatabaseErrorHandler);
+ * db.enableWriteAheadLogging();
+ * </pre></code>
+ * </p><p>
+ * Another way to enable write-ahead logging is to call {@code enableWriteAheadLogging}
+ * after opening the database.
+ * <code><pre>
+ * SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+ * SQLiteDatabase.CREATE_IF_NECESSARY, myDatabaseErrorHandler);
+ * db.enableWriteAheadLogging();
+ * </pre></code>
+ * </p><p>
+ * See also <a href="http://sqlite.org/wal.html">SQLite Write-Ahead Logging</a> for
+ * more details about how write-ahead logging works.
+ * </p>
+ *
+ * @return True if write-ahead logging is enabled.
+ * @throws IllegalStateException if there are transactions in progress at the
+ * time this method is called. WAL mode can only be changed when
+ * there are no
+ * transactions in progress.
+ * @see SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING
+ * @see #disableWriteAheadLogging
+ */
+ boolean enableWriteAheadLogging();
+
+ /**
+ * This method disables the features enabled by {@link #enableWriteAheadLogging()}.
+ *
+ * @throws IllegalStateException if there are transactions in progress at the
+ * time this method is called. WAL mode can only be changed when
+ * there are no
+ * transactions in progress.
+ * @see #enableWriteAheadLogging
+ */
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ void disableWriteAheadLogging();
+
+ /**
+ * Returns true if write-ahead logging has been enabled for this database.
+ *
+ * @return True if write-ahead logging has been enabled for this database.
+ * @see #enableWriteAheadLogging
+ * @see SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING
+ */
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ boolean isWriteAheadLoggingEnabled();
+
+ /**
+ * Returns list of full path names of all attached databases including the main database
+ * by executing 'pragma database_list' on the database.
+ *
+ * @return ArrayList of pairs of (database name, database file path) or null if the database
+ * is not open.
+ */
+ List<Pair<String, String>> getAttachedDbs();
+
+ /**
+ * Runs 'pragma integrity_check' on the given database (and all the attached databases)
+ * and returns true if the given database (and all its attached databases) pass integrity_check,
+ * false otherwise.
+ * <p>
+ * If the result is false, then this method logs the errors reported by the integrity_check
+ * command execution.
+ * <p>
+ * Note that 'pragma integrity_check' on a database can take a long time.
+ *
+ * @return true if the given database (and all its attached databases) pass integrity_check,
+ * false otherwise.
+ */
+ boolean isDatabaseIntegrityOk();
+}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
new file mode 100644
index 0000000..322f9d5
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}.
+ * Note that since that class requires overriding certain methods, support implementation
+ * uses {@link Factory#create(Configuration)} to create this and {@link Callback} to implement
+ * the methods that should be overridden.
+ */
+@SuppressWarnings("unused")
+public interface SupportSQLiteOpenHelper {
+ /**
+ * Return the name of the SQLite database being opened, as given to
+ * the constructor.
+ */
+ String getDatabaseName();
+
+ /**
+ * Enables or disables the use of write-ahead logging for the database.
+ *
+ * Write-ahead logging cannot be used with read-only databases so the value of
+ * this flag is ignored if the database is opened read-only.
+ *
+ * @param enabled True if write-ahead logging should be enabled, false if it
+ * should be disabled.
+ * @see SupportSQLiteDatabase#enableWriteAheadLogging()
+ */
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ void setWriteAheadLoggingEnabled(boolean enabled);
+
+ /**
+ * Create and/or open a database that will be used for reading and writing.
+ * The first time this is called, the database will be opened and
+ * {@link Callback#onCreate}, {@link Callback#onUpgrade} and/or {@link Callback#onOpen} will be
+ * called.
+ *
+ * <p>Once opened successfully, the database is cached, so you can
+ * call this method every time you need to write to the database.
+ * (Make sure to call {@link #close} when you no longer need the database.)
+ * Errors such as bad permissions or a full disk may cause this method
+ * to fail, but future attempts may succeed if the problem is fixed.</p>
+ *
+ * <p class="caution">Database upgrade may take a long time, you
+ * should not call this method from the application main thread, including
+ * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
+ *
+ * @return a read/write database object valid until {@link #close} is called
+ * @throws SQLiteException if the database cannot be opened for writing
+ */
+ SupportSQLiteDatabase getWritableDatabase();
+
+ /**
+ * Create and/or open a database. This will be the same object returned by
+ * {@link #getWritableDatabase} unless some problem, such as a full disk,
+ * requires the database to be opened read-only. In that case, a read-only
+ * database object will be returned. If the problem is fixed, a future call
+ * to {@link #getWritableDatabase} may succeed, in which case the read-only
+ * database object will be closed and the read/write object will be returned
+ * in the future.
+ *
+ * <p class="caution">Like {@link #getWritableDatabase}, this method may
+ * take a long time to return, so you should not call it from the
+ * application main thread, including from
+ * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}.
+ *
+ * @return a database object valid until {@link #getWritableDatabase}
+ * or {@link #close} is called.
+ * @throws SQLiteException if the database cannot be opened
+ */
+ SupportSQLiteDatabase getReadableDatabase();
+
+ /**
+ * Close any open database object.
+ */
+ void close();
+
+ /**
+ * 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.
+ * <p>
+ * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade},
+ * or {@link #onOpen} are called. It should not modify the database except to configure the
+ * database connection as required.
+ * </p>
+ * <p>
+ * This method should only call methods that configure the parameters of the database
+ * connection, such as {@link SupportSQLiteDatabase#enableWriteAheadLogging}
+ * {@link SupportSQLiteDatabase#setForeignKeyConstraintsEnabled},
+ * {@link SupportSQLiteDatabase#setLocale},
+ * {@link SupportSQLiteDatabase#setMaximumSize}, or executing PRAGMA statements.
+ * </p>
+ *
+ * @param db The database.
+ */
+ public void onConfigure(SupportSQLiteDatabase db) {
+
+ }
+
+ /**
+ * Called when the database is created for the first time. This is where the
+ * creation of tables and the initial population of the tables should happen.
+ *
+ * @param db The database.
+ */
+ public abstract void onCreate(SupportSQLiteDatabase db);
+
+ /**
+ * Called when the database needs to be upgraded. The implementation
+ * should use this method to drop tables, add tables, or do anything else it
+ * needs to upgrade to the new schema version.
+ *
+ * <p>
+ * The SQLite ALTER TABLE documentation can be found
+ * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
+ * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
+ * you can use ALTER TABLE to rename the old table, then create the new table and then
+ * populate the new table with the contents of the old table.
+ * </p><p>
+ * This method executes within a transaction. If an exception is thrown, all changes
+ * will automatically be rolled back.
+ * </p>
+ *
+ * @param db The database.
+ * @param oldVersion The old database version.
+ * @param newVersion The new database version.
+ */
+ public abstract void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion);
+
+ /**
+ * Called when the database needs to be downgraded. This is strictly similar to
+ * {@link #onUpgrade} method, but is called whenever current version is newer than requested
+ * one.
+ * However, this method is not abstract, so it is not mandatory for a customer to
+ * implement it. If not overridden, default implementation will reject downgrade and
+ * throws SQLiteException
+ *
+ * <p>
+ * This method executes within a transaction. If an exception is thrown, all changes
+ * will automatically be rolled back.
+ * </p>
+ *
+ * @param db The database.
+ * @param oldVersion The old database version.
+ * @param newVersion The new database version.
+ */
+ public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ throw new SQLiteException("Can't downgrade database from version "
+ + oldVersion + " to " + newVersion);
+ }
+
+ /**
+ * Called when the database has been opened. The implementation
+ * should check {@link SupportSQLiteDatabase#isReadOnly} before updating the
+ * database.
+ * <p>
+ * This method is called after the database connection has been configured
+ * and after the database schema has been created, upgraded or downgraded as necessary.
+ * If the database connection must be configured in some way before the schema
+ * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead.
+ * </p>
+ *
+ * @param db The database.
+ */
+ 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);
+ }
+ }
+ }
+
+ /**
+ * The configuration to create an SQLite open helper object using {@link Factory}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ class Configuration {
+ /**
+ * Context to use to open or create the database.
+ */
+ @NonNull
+ public final Context context;
+ /**
+ * Name of the database file, or null for an in-memory database.
+ */
+ @Nullable
+ public final String name;
+ /**
+ * The callback class to handle creation, upgrade and downgrade.
+ */
+ @NonNull
+ public final SupportSQLiteOpenHelper.Callback callback;
+
+ Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) {
+ this.context = context;
+ this.name = name;
+ this.callback = callback;
+ }
+
+ /**
+ * Creates a new Configuration.Builder to create an instance of Configuration.
+ *
+ * @param context to use to open or create the database.
+ */
+ public static Builder builder(Context context) {
+ return new Builder(context);
+ }
+
+ /**
+ * Builder class for {@link Configuration}.
+ */
+ public static class Builder {
+ Context mContext;
+ String mName;
+ SupportSQLiteOpenHelper.Callback mCallback;
+
+ public Configuration build() {
+ if (mCallback == null) {
+ throw new IllegalArgumentException("Must set a callback to create the"
+ + " configuration.");
+ }
+ if (mContext == null) {
+ throw new IllegalArgumentException("Must set a non-null context to create"
+ + " the configuration.");
+ }
+ return new Configuration(mContext, mName, mCallback);
+ }
+
+ Builder(@NonNull Context context) {
+ mContext = context;
+ }
+
+ /**
+ * @param name Name of the database file, or null for an in-memory database.
+ * @return This
+ */
+ public Builder name(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * @param callback The callback class to handle creation, upgrade and downgrade.
+ * @return this
+ */
+ public Builder callback(@NonNull Callback callback) {
+ mCallback = callback;
+ return this;
+ }
+ }
+ }
+
+ /**
+ * Factory class to create instances of {@link SupportSQLiteOpenHelper} using
+ * {@link Configuration}.
+ */
+ interface Factory {
+ /**
+ * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration.
+ *
+ * @param configuration The configuration to use while creating the open helper.
+ *
+ * @return A SupportSQLiteOpenHelper which can be used to open a database.
+ */
+ SupportSQLiteOpenHelper create(Configuration configuration);
+ }
+}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java
new file mode 100644
index 0000000..9eeb9b0
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db;
+
+import java.io.Closeable;
+
+/**
+ * An interface to map the behavior of {@link android.database.sqlite.SQLiteProgram}.
+ */
+
+@SuppressWarnings("unused")
+public interface SupportSQLiteProgram extends Closeable {
+ /**
+ * Bind a NULL value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind null to
+ */
+ void bindNull(int index);
+
+ /**
+ * Bind a long value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *addToBindArgs
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind
+ */
+ void bindLong(int index, long value);
+
+ /**
+ * Bind a double value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind
+ */
+ void bindDouble(int index, double value);
+
+ /**
+ * Bind a String value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind, must not be null
+ */
+ void bindString(int index, String value);
+
+ /**
+ * Bind a byte array value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind, must not be null
+ */
+ void bindBlob(int index, byte[] value);
+
+ /**
+ * Clears all existing bindings. Unset bindings are treated as NULL.
+ */
+ void clearBindings();
+}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java
new file mode 100644
index 0000000..b631528
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.java
@@ -0,0 +1,46 @@
+/*
+ * 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 androidx.sqlite.db;
+
+/**
+ * A query with typed bindings. It is better to use this API instead of
+ * {@link android.database.sqlite.SQLiteDatabase#rawQuery(String, String[])} because it allows
+ * binding type safe parameters.
+ */
+public interface SupportSQLiteQuery {
+ /**
+ * The SQL query. This query can have placeholders(?) for bind arguments.
+ *
+ * @return The SQL query to compile
+ */
+ String getSql();
+
+ /**
+ * Callback to bind the query parameters to the compiled statement.
+ *
+ * @param statement The compiled statement
+ */
+ void bindTo(SupportSQLiteProgram statement);
+
+ /**
+ * Returns the number of arguments in this query. This is equal to the number of placeholders
+ * in the query string. See: https://www.sqlite.org/c3ref/bind_blob.html for details.
+ *
+ * @return The number of arguments in the query.
+ */
+ int getArgCount();
+}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.java b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.java
new file mode 100644
index 0000000..a438fa8
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.java
@@ -0,0 +1,202 @@
+/*
+ * 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 androidx.sqlite.db;
+
+import java.util.regex.Pattern;
+
+/**
+ * A simple query builder to create SQL SELECT queries.
+ */
+@SuppressWarnings("unused")
+public final class SupportSQLiteQueryBuilder {
+ private static final Pattern sLimitPattern =
+ Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
+
+ private boolean mDistinct = false;
+ private final String mTable;
+ private String[] mColumns = null;
+ private String mSelection;
+ private Object[] mBindArgs;
+ private String mGroupBy = null;
+ private String mHaving = null;
+ private String mOrderBy = null;
+ private String mLimit = null;
+
+ /**
+ * Creates a query for the given table name.
+ *
+ * @param tableName The table name(s) to query.
+ *
+ * @return A builder to create a query.
+ */
+ public static SupportSQLiteQueryBuilder builder(String tableName) {
+ return new SupportSQLiteQueryBuilder(tableName);
+ }
+
+ private SupportSQLiteQueryBuilder(String table) {
+ mTable = table;
+ }
+
+ /**
+ * Adds DISTINCT keyword to the query.
+ *
+ * @return this
+ */
+ public SupportSQLiteQueryBuilder distinct() {
+ mDistinct = true;
+ return this;
+ }
+
+ /**
+ * Sets the given list of columns as the columns that will be returned.
+ *
+ * @param columns The list of column names that should be returned.
+ *
+ * @return this
+ */
+ public SupportSQLiteQueryBuilder columns(String[] columns) {
+ mColumns = columns;
+ return this;
+ }
+
+ /**
+ * Sets the arguments for the WHERE clause.
+ *
+ * @param selection The list of selection columns
+ * @param bindArgs The list of bind arguments to match against these columns
+ *
+ * @return this
+ */
+ public SupportSQLiteQueryBuilder selection(String selection, Object[] bindArgs) {
+ mSelection = selection;
+ mBindArgs = bindArgs;
+ return this;
+ }
+
+ /**
+ * Adds a GROUP BY statement.
+ *
+ * @param groupBy The value of the GROUP BY statement.
+ *
+ * @return this
+ */
+ @SuppressWarnings("WeakerAccess")
+ public SupportSQLiteQueryBuilder groupBy(String groupBy) {
+ mGroupBy = groupBy;
+ return this;
+ }
+
+ /**
+ * Adds a HAVING statement. You must also provide {@link #groupBy(String)} for this to work.
+ *
+ * @param having The having clause.
+ *
+ * @return this
+ */
+ public SupportSQLiteQueryBuilder having(String having) {
+ mHaving = having;
+ return this;
+ }
+
+ /**
+ * Adds an ORDER BY statement.
+ *
+ * @param orderBy The order clause.
+ *
+ * @return this
+ */
+ public SupportSQLiteQueryBuilder orderBy(String orderBy) {
+ mOrderBy = orderBy;
+ return this;
+ }
+
+ /**
+ * Adds a LIMIT statement.
+ *
+ * @param limit The limit value.
+ *
+ * @return this
+ */
+ public SupportSQLiteQueryBuilder limit(String limit) {
+ if (!isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
+ throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
+ }
+ mLimit = limit;
+ return this;
+ }
+
+ /**
+ * Creates the {@link SupportSQLiteQuery} that can be passed into
+ * {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
+ *
+ * @return a new query
+ */
+ public SupportSQLiteQuery create() {
+ if (isEmpty(mGroupBy) && !isEmpty(mHaving)) {
+ throw new IllegalArgumentException(
+ "HAVING clauses are only permitted when using a groupBy clause");
+ }
+ StringBuilder query = new StringBuilder(120);
+
+ query.append("SELECT ");
+ if (mDistinct) {
+ query.append("DISTINCT ");
+ }
+ if (mColumns != null && mColumns.length != 0) {
+ appendColumns(query, mColumns);
+ } else {
+ query.append(" * ");
+ }
+ query.append(" FROM ");
+ query.append(mTable);
+ appendClause(query, " WHERE ", mSelection);
+ appendClause(query, " GROUP BY ", mGroupBy);
+ appendClause(query, " HAVING ", mHaving);
+ appendClause(query, " ORDER BY ", mOrderBy);
+ appendClause(query, " LIMIT ", mLimit);
+
+ return new SimpleSQLiteQuery(query.toString(), mBindArgs);
+ }
+
+ private static void appendClause(StringBuilder s, String name, String clause) {
+ if (!isEmpty(clause)) {
+ s.append(name);
+ s.append(clause);
+ }
+ }
+
+ /**
+ * Add the names that are non-null in columns to s, separating
+ * them with commas.
+ */
+ private static void appendColumns(StringBuilder s, String[] columns) {
+ int n = columns.length;
+
+ for (int i = 0; i < n; i++) {
+ String column = columns[i];
+ if (i > 0) {
+ s.append(", ");
+ }
+ s.append(column);
+ }
+ s.append(' ');
+ }
+
+ private static boolean isEmpty(String input) {
+ return input == null || input.length() == 0;
+ }
+}
diff --git a/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java
new file mode 100644
index 0000000..1b136d5
--- /dev/null
+++ b/persistence/db/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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 androidx.sqlite.db;
+
+/**
+ * An interface to map the behavior of {@link android.database.sqlite.SQLiteStatement}.
+ */
+@SuppressWarnings("unused")
+public interface SupportSQLiteStatement extends SupportSQLiteProgram {
+ /**
+ * Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example
+ * CREATE / DROP table, view, trigger, index etc.
+ *
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ void execute();
+
+ /**
+ * Execute this SQL statement, if the the number of rows affected by execution of this SQL
+ * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
+ *
+ * @return the number of rows affected by this SQL statement execution.
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ int executeUpdateDelete();
+
+ /**
+ * Execute this SQL statement and return the ID of the row inserted due to this call.
+ * The SQL statement should be an INSERT for this to be a useful call.
+ *
+ * @return the row ID of the last row inserted, if this insert is successful. -1 otherwise.
+ *
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ long executeInsert();
+
+ /**
+ * Execute a statement that returns a 1 by 1 table with a numeric value.
+ * For example, SELECT COUNT(*) FROM table;
+ *
+ * @return The result of the query.
+ *
+ * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+ */
+ long simpleQueryForLong();
+ /**
+ * Execute a statement that returns a 1 by 1 table with a text value.
+ * For example, SELECT COUNT(*) FROM table;
+ *
+ * @return The result of the query.
+ *
+ * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+ */
+ String simpleQueryForString();
+}
diff --git a/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java b/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java
index 1c9ebe0..2fa0e1b 100644
--- a/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java
+++ b/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragment.java
@@ -62,7 +62,7 @@
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
- mEditText = (EditText) view.findViewById(android.R.id.edit);
+ mEditText = view.findViewById(android.R.id.edit);
if (mEditText == null) {
throw new IllegalStateException("Dialog view must contain an EditText with id"
@@ -70,6 +70,8 @@
}
mEditText.setText(mText);
+ // Place cursor at the end
+ mEditText.setSelection(mText.length());
}
private EditTextPreference getEditTextPreference() {
diff --git a/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java b/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java
index d02b95b..45c62be 100644
--- a/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java
+++ b/preference/src/main/java/androidx/preference/EditTextPreferenceDialogFragmentCompat.java
@@ -62,7 +62,7 @@
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
- mEditText = (EditText) view.findViewById(android.R.id.edit);
+ mEditText = view.findViewById(android.R.id.edit);
if (mEditText == null) {
throw new IllegalStateException("Dialog view must contain an EditText with id" +
@@ -70,6 +70,8 @@
}
mEditText.setText(mText);
+ // Place cursor at the end
+ mEditText.setSelection(mText.length());
}
private EditTextPreference getEditTextPreference() {
diff --git a/preference/src/main/java/androidx/preference/ListPreferenceDialogFragmentCompat.java b/preference/src/main/java/androidx/preference/ListPreferenceDialogFragmentCompat.java
index 2e2960d..9a7cf66 100644
--- a/preference/src/main/java/androidx/preference/ListPreferenceDialogFragmentCompat.java
+++ b/preference/src/main/java/androidx/preference/ListPreferenceDialogFragmentCompat.java
@@ -22,8 +22,6 @@
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
-import java.util.ArrayList;
-
public class ListPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {
private static final String SAVE_STATE_INDEX = "ListPreferenceDialogFragment.index";
@@ -60,8 +58,8 @@
mEntryValues = preference.getEntryValues();
} else {
mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0);
- mEntries = getCharSequenceArray(savedInstanceState, SAVE_STATE_ENTRIES);
- mEntryValues = getCharSequenceArray(savedInstanceState, SAVE_STATE_ENTRY_VALUES);
+ mEntries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES);
+ mEntryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES);
}
}
@@ -69,24 +67,8 @@
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex);
- putCharSequenceArray(outState, SAVE_STATE_ENTRIES, mEntries);
- putCharSequenceArray(outState, SAVE_STATE_ENTRY_VALUES, mEntryValues);
- }
-
- private static void putCharSequenceArray(Bundle out, String key, CharSequence[] entries) {
- final ArrayList<String> stored = new ArrayList<>(entries.length);
-
- for (final CharSequence cs : entries) {
- stored.add(cs.toString());
- }
-
- out.putStringArrayList(key, stored);
- }
-
- private static CharSequence[] getCharSequenceArray(Bundle in, String key) {
- final ArrayList<String> stored = in.getStringArrayList(key);
-
- return stored == null ? null : stored.toArray(new CharSequence[stored.size()]);
+ outState.putCharSequenceArray(SAVE_STATE_ENTRIES, mEntries);
+ outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues);
}
private ListPreference getListPreference() {
diff --git a/room/common/api/1.0.0.txt b/room/common/api_legacy/1.0.0.txt
similarity index 100%
rename from room/common/api/1.0.0.txt
rename to room/common/api_legacy/1.0.0.txt
diff --git a/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java b/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java
deleted file mode 100644
index 8d0b7ce..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/ColumnInfo.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.support.annotation.IntDef;
-import android.support.annotation.RequiresApi;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Allows specific customization about the column associated with this field.
- * <p>
- * For example, you can specify a column name for the field or change the column's type affinity.
- */
-@Target(ElementType.FIELD)
-@Retention(RetentionPolicy.CLASS)
-public @interface ColumnInfo {
- /**
- * Name of the column in the database. Defaults to the field name if not set.
- *
- * @return Name of the column in the database.
- */
- String name() default INHERIT_FIELD_NAME;
-
- /**
- * The type affinity for the column, which will be used when constructing the database.
- * <p>
- * If it is not specified, the value defaults to {@link #UNDEFINED} and Room resolves it based
- * on the field's type and available TypeConverters.
- * <p>
- * See <a href="https://www.sqlite.org/datatype3.html">SQLite types documentation</a> for
- * details.
- *
- * @return The type affinity of the column. This is either {@link #UNDEFINED}, {@link #TEXT},
- * {@link #INTEGER}, {@link #REAL}, or {@link #BLOB}.
- */
- @SuppressWarnings("unused") @SQLiteTypeAffinity int typeAffinity() default UNDEFINED;
-
- /**
- * Convenience method to index the field.
- * <p>
- * If you would like to create a composite index instead, see: {@link Index}.
- *
- * @return True if this field should be indexed, false otherwise. Defaults to false.
- */
- boolean index() default false;
-
- /**
- * The collation sequence for the column, which will be used when constructing the database.
- * <p>
- * The default value is {@link #UNSPECIFIED}. In that case, Room does not add any
- * collation sequence to the column, and SQLite treats it like {@link #BINARY}.
- *
- * @return The collation sequence of the column. This is either {@link #UNSPECIFIED},
- * {@link #BINARY}, {@link #NOCASE}, {@link #RTRIM}, {@link #LOCALIZED} or {@link #UNICODE}.
- */
- @Collate int collate() default UNSPECIFIED;
-
- /**
- * Constant to let Room inherit the field name as the column name. If used, Room will use the
- * field name as the column name.
- */
- String INHERIT_FIELD_NAME = "[field-name]";
-
- /**
- * Undefined type affinity. Will be resolved based on the type.
- *
- * @see #typeAffinity()
- */
- int UNDEFINED = 1;
- /**
- * Column affinity constant for strings.
- *
- * @see #typeAffinity()
- */
- int TEXT = 2;
- /**
- * Column affinity constant for integers or booleans.
- *
- * @see #typeAffinity()
- */
- int INTEGER = 3;
- /**
- * Column affinity constant for floats or doubles.
- *
- * @see #typeAffinity()
- */
- int REAL = 4;
- /**
- * Column affinity constant for binary data.
- *
- * @see #typeAffinity()
- */
- int BLOB = 5;
-
- /**
- * The SQLite column type constants that can be used in {@link #typeAffinity()}
- */
- @IntDef({UNDEFINED, TEXT, INTEGER, REAL, BLOB})
- @interface SQLiteTypeAffinity {
- }
-
- /**
- * Collation sequence is not specified. The match will behave like {@link #BINARY}.
- *
- * @see #collate()
- */
- int UNSPECIFIED = 1;
- /**
- * Collation sequence for case-sensitive match.
- *
- * @see #collate()
- */
- int BINARY = 2;
- /**
- * Collation sequence for case-insensitive match.
- *
- * @see #collate()
- */
- int NOCASE = 3;
- /**
- * Collation sequence for case-sensitive match except that trailing space characters are
- * ignored.
- *
- * @see #collate()
- */
- int RTRIM = 4;
- /**
- * Collation sequence that uses system's current locale.
- *
- * @see #collate()
- */
- @RequiresApi(21)
- int LOCALIZED = 5;
- /**
- * Collation sequence that uses Unicode Collation Algorithm.
- *
- * @see #collate()
- */
- @RequiresApi(21)
- int UNICODE = 6;
-
- @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
- @interface Collate {
- }
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Dao.java b/room/common/src/main/java/android/arch/persistence/room/Dao.java
deleted file mode 100644
index ea205e6..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Dao.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2016 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 the class as a Data Access Object.
- * <p>
- * Data Access Objects are the main classes where you define your database interactions. They can
- * include a variety of query methods.
- * <p>
- * The class marked with {@code @Dao} should either be an interface or an abstract class. At compile
- * time, Room will generate an implementation of this class when it is referenced by a
- * {@link Database}.
- * <p>
- * An abstract {@code @Dao} class can optionally have a constructor that takes a {@link Database}
- * as its only parameter.
- * <p>
- * It is recommended to have multiple {@code Dao} classes in your codebase depending on the tables
- * they touch.
- *
- * @see Query
- * @see Delete
- * @see Insert
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.CLASS)
-public @interface Dao {
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Database.java b/room/common/src/main/java/android/arch/persistence/room/Database.java
deleted file mode 100644
index 14e722f..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Database.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2016 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 class as a RoomDatabase.
- * <p>
- * The class should be an abstract class and extend
- * {@link android.arch.persistence.room.RoomDatabase RoomDatabase}.
- * <p>
- * You can receive an implementation of the class via
- * {@link android.arch.persistence.room.Room#databaseBuilder Room.databaseBuilder} or
- * {@link android.arch.persistence.room.Room#inMemoryDatabaseBuilder Room.inMemoryDatabaseBuilder}.
- * <p>
- * <pre>
- * // User and Book are classes annotated with {@literal @}Entity.
- * {@literal @}Database(version = 1, entities = {User.class, Book.class})
- * abstract class AppDatabase extends RoomDatabase {
- * // BookDao is a class annotated with {@literal @}Dao.
- * abstract public BookDao bookDao();
- * // UserDao is a class annotated with {@literal @}Dao.
- * abstract public UserDao userDao();
- * // UserBookDao is a class annotated with {@literal @}Dao.
- * abstract public UserBookDao userBookDao();
- * }
- * </pre>
- * The example above defines a class that has 2 tables and 3 DAO classes that are used to access it.
- * There is no limit on the number of {@link Entity} or {@link Dao} classes but they must be unique
- * within the Database.
- * <p>
- * Instead of running queries on the database directly, you are highly recommended to create
- * {@link Dao} classes. Using Dao classes will allow you to abstract the database communication in
- * a more logical layer which will be much easier to mock in tests (compared to running direct
- * sql queries). It also automatically does the conversion from {@code Cursor} to your application
- * classes so you don't need to deal with lower level database APIs for most of your data access.
- * <p>
- * Room also verifies all of your queries in {@link Dao} classes while the application is being
- * compiled so that if there is a problem in one of the queries, you will be notified instantly.
- * @see Dao
- * @see Entity
- * @see android.arch.persistence.room.RoomDatabase RoomDatabase
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.CLASS)
-public @interface Database {
- /**
- * The list of entities included in the database. Each entity turns into a table in the
- * database.
- *
- * @return The list of entities in the database.
- */
- Class[] entities();
-
- /**
- * The database version.
- *
- * @return The database version.
- */
- int version();
-
- /**
- * You can set annotation processor argument ({@code room.schemaLocation})
- * to tell Room to export the schema into a folder. Even though it is not mandatory, it is a
- * good practice to have version history in your codebase and you should commit that file into
- * your version control system (but don't ship it with your app!).
- * <p>
- * When {@code room.schemaLocation} is set, Room will check this variable and if it is set to
- * {@code true}, its schema will be exported into the given folder.
- * <p>
- * {@code exportSchema} is {@code true} by default but you can disable it for databases when
- * you don't want to keep history of versions (like an in-memory only database).
- *
- * @return Whether the schema should be exported to the given folder when the
- * {@code room.schemaLocation} argument is set. Defaults to {@code true}.
- */
- boolean exportSchema() default true;
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Delete.java b/room/common/src/main/java/android/arch/persistence/room/Delete.java
deleted file mode 100644
index 678b743..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Delete.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 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 a {@link Dao} annotated class as a delete method.
- * <p>
- * The implementation of the method will delete its parameters from the database.
- * <p>
- * All of the parameters of the Delete method must either be classes annotated with {@link Entity}
- * or collections/array of it.
- * <p>
- * Example:
- * <pre>
- * {@literal @}Dao
- * public interface MyDao {
- * {@literal @}Delete
- * public void deleteUsers(User... users);
- * {@literal @}Delete
- * public void deleteAll(User user1, User user2);
- * {@literal @}Delete
- * public void deleteWithFriends(User user, List<User> friends);
- * }
- * </pre>
- *
- * @see Insert
- * @see Query
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.CLASS)
-public @interface Delete {
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Embedded.java b/room/common/src/main/java/android/arch/persistence/room/Embedded.java
deleted file mode 100644
index 781f026..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Embedded.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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;
-
-/**
- * Can be used as an annotation on a field of an {@link Entity} or {@code Pojo} to signal that
- * nested fields (i.e. fields of the annotated field's class) can be referenced directly in the SQL
- * queries.
- * <p>
- * If the container is an {@link Entity}, these sub fields will be columns in the {@link Entity}'s
- * database table.
- * <p>
- * For example, if you have 2 classes:
- * <pre>
- * public class Coordinates {
- * double latitude;
- * double longitude;
- * }
- * public class Address {
- * String street;
- * {@literal @}Embedded
- * Coordinates coordinates;
- * }
- * </pre>
- * Room will consider {@code latitude} and {@code longitude} as if they are fields of the
- * {@code Address} class when mapping an SQLite row to {@code Address}.
- * <p>
- * So if you have a query that returns {@code street, latitude, longitude}, Room will properly
- * construct an {@code Address} class.
- * <p>
- * If the {@code Address} class is annotated with {@link Entity}, its database table will have 3
- * columns: {@code street, latitude, longitude}
- * <p>
- * If there is a name conflict with the fields of the sub object and the owner object, you can
- * specify a {@link #prefix()} for the items of the sub object. Note that prefix is always applied
- * to sub fields even if they have a {@link ColumnInfo} with a specific {@code name}.
- * <p>
- * If sub fields of an embedded field has {@link PrimaryKey} annotation, they <b>will not</b> be
- * considered as primary keys in the owner {@link Entity}.
- * <p>
- * When an embedded field is read, if all fields of the embedded field (and its sub fields) are
- * {@code null} in the {@link android.database.Cursor Cursor}, it is set to {@code null}. Otherwise,
- * it is constructed.
- * <p>
- * Note that even if you have {@link TypeConverter}s that convert a {@code null} column into a
- * {@code non-null} value, if all columns of the embedded field in the
- * {@link android.database.Cursor Cursor} are null, the {@link TypeConverter} will never be called
- * and the embedded field will not be constructed.
- * <p>
- * You can override this behavior by annotating the embedded field with
- * {@link android.support.annotation.NonNull}.
- */
-@Target(ElementType.FIELD)
-@Retention(RetentionPolicy.CLASS)
-public @interface Embedded {
- /**
- * Specifies a prefix to prepend the column names of the fields in the embedded fields.
- * <p>
- * For the example above, if we've written:
- * <pre>
- * {@literal @}Embedded(prefix = "foo_")
- * Coordinates coordinates;
- * </pre>
- * The column names for {@code latitude} and {@code longitude} will be {@code foo_latitude} and
- * {@code foo_longitude} respectively.
- * <p>
- * By default, prefix is the empty string.
- *
- * @return The prefix to be used for the fields of the embedded item.
- */
- String prefix() default "";
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Entity.java b/room/common/src/main/java/android/arch/persistence/room/Entity.java
deleted file mode 100644
index 94ca3bf..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Entity.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2016 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 class as an entity. This class will have a mapping SQLite table in the database.
- * <p>
- * Each entity must have at least 1 field annotated with {@link PrimaryKey}.
- * You can also use {@link #primaryKeys()} attribute to define the primary key.
- * <p>
- * Each entity must either have a no-arg constructor or a constructor whose parameters match
- * fields (based on type and name). Constructor does not have to receive all fields as parameters
- * but if a field is not passed into the constructor, it should either be public or have a public
- * setter. If a matching constructor is available, Room will always use it. If you don't want it
- * to use a constructor, you can annotate it with {@link Ignore}.
- * <p>
- * 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
- * public class User {
- * {@literal @}PrimaryKey
- * private final int uid;
- * private String name;
- * {@literal @}ColumnInfo(name = "last_name")
- * private String lastName;
- *
- * public User(int uid) {
- * this.uid = uid;
- * }
- * public String getLastName() {
- * return lastName;
- * }
- * public void setLastName(String lastName) {
- * this.lastName = lastName;
- * }
- * }
- * </pre>
- *
- * @see Dao
- * @see Database
- * @see PrimaryKey
- * @see ColumnInfo
- * @see Index
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.CLASS)
-public @interface Entity {
- /**
- * The table name in the SQLite database. If not set, defaults to the class name.
- *
- * @return The SQLite tableName of the Entity.
- */
- String tableName() default "";
-
- /**
- * List of indices on the table.
- *
- * @return The list of indices on the table.
- */
- Index[] indices() default {};
-
- /**
- * If set to {@code true}, any Index defined in parent classes of this class will be carried
- * over to the current {@code Entity}. Note that if you set this to {@code true}, even if the
- * {@code Entity} has a parent which sets this value to {@code false}, the {@code Entity} will
- * still inherit indices from it and its parents.
- * <p>
- * When the {@code Entity} inherits an index from the parent, it is <b>always</b> renamed with
- * the default naming schema since SQLite <b>does not</b> allow using the same index name in
- * multiple tables. See {@link Index} for the details of the default name.
- * <p>
- * By default, indices defined in parent classes are dropped to avoid unexpected indices.
- * When this happens, you will receive a {@link RoomWarnings#INDEX_FROM_PARENT_FIELD_IS_DROPPED}
- * or {@link RoomWarnings#INDEX_FROM_PARENT_IS_DROPPED} warning during compilation.
- *
- * @return True if indices from parent classes should be automatically inherited by this Entity,
- * false otherwise. Defaults to false.
- */
- boolean inheritSuperIndices() default false;
-
- /**
- * The list of Primary Key column names.
- * <p>
- * If you would like to define an auto generated primary key, you can use {@link PrimaryKey}
- * annotation on the field with {@link PrimaryKey#autoGenerate()} set to {@code true}.
- *
- * @return The primary key of this Entity. Can be empty if the class has a field annotated
- * with {@link PrimaryKey}.
- */
- String[] primaryKeys() default {};
-
- /**
- * List of {@link ForeignKey} constraints on this entity.
- *
- * @return The list of {@link ForeignKey} constraints on this entity.
- */
- ForeignKey[] foreignKeys() default {};
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/ForeignKey.java b/room/common/src/main/java/android/arch/persistence/room/ForeignKey.java
deleted file mode 100644
index 3ba632b..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/ForeignKey.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * 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 android.support.annotation.IntDef;
-
-/**
- * Declares a foreign key on another {@link Entity}.
- * <p>
- * Foreign keys allows you to specify constraints across Entities such that SQLite will ensure that
- * the relationship is valid when you modify the database.
- * <p>
- * When a foreign key constraint is specified, SQLite requires the referenced columns to be part of
- * a unique index in the parent table or the primary key of that table. You must create a unique
- * index in the parent entity that covers the referenced columns (Room will verify this at compile
- * time and print an error if it is missing).
- * <p>
- * It is also recommended to create an index on the child table to avoid full table scans when the
- * parent table is modified. If a suitable index on the child table is missing, Room will print
- * {@link RoomWarnings#MISSING_INDEX_ON_FOREIGN_KEY_CHILD} warning.
- * <p>
- * A foreign key constraint can be deferred until the transaction is complete. This is useful if
- * you are doing bulk inserts into the database in a single transaction. By default, foreign key
- * constraints are immediate but you can change this value by setting {@link #deferred()} to
- * {@code true}. You can also use
- * <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>
- * documentation for details.
- */
-public @interface ForeignKey {
- /**
- * The parent Entity to reference. It must be a class annotated with {@link Entity} and
- * referenced in the same database.
- *
- * @return The parent Entity.
- */
- Class entity();
-
- /**
- * The list of column names in the parent {@link Entity}.
- * <p>
- * Number of columns must match the number of columns specified in {@link #childColumns()}.
- *
- * @return The list of column names in the parent Entity.
- * @see #childColumns()
- */
- String[] parentColumns();
-
- /**
- * The list of column names in the current {@link Entity}.
- * <p>
- * Number of columns must match the number of columns specified in {@link #parentColumns()}.
- *
- * @return The list of column names in the current Entity.
- */
- String[] childColumns();
-
- /**
- * Action to take when the parent {@link Entity} is deleted from the database.
- * <p>
- * By default, {@link #NO_ACTION} is used.
- *
- * @return The action to take when the referenced entity is deleted from the database.
- */
- @Action int onDelete() default NO_ACTION;
-
- /**
- * Action to take when the parent {@link Entity} is updated in the database.
- * <p>
- * By default, {@link #NO_ACTION} is used.
- *
- * @return The action to take when the referenced entity is updated in the database.
- */
- @Action int onUpdate() default NO_ACTION;
-
- /**
- * * A foreign key constraint can be deferred until the transaction is complete. This is useful
- * if you are doing bulk inserts into the database in a single transaction. By default, foreign
- * key constraints are immediate but you can change it by setting this field to {@code true}.
- * You can also use
- * <a href="https://sqlite.org/pragma.html#pragma_defer_foreign_keys">defer_foreign_keys</a>
- * PRAGMA to defer them depending on your transaction.
- *
- * @return Whether the foreign key constraint should be deferred until the transaction is
- * complete. Defaults to {@code false}.
- */
- boolean deferred() default false;
-
- /**
- * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
- * <p>
- * When a parent key is modified or deleted from the database, no special action is taken.
- * This means that SQLite will not make any effort to fix the constraint failure, instead,
- * reject the change.
- */
- int NO_ACTION = 1;
-
- /**
- * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
- * <p>
- * The RESTRICT action means that the application is prohibited from deleting
- * (for {@link #onDelete()}) or modifying (for {@link #onUpdate()}) a parent key when there
- * exists one or more child keys mapped to it. The difference between the effect of a RESTRICT
- * action and normal foreign key constraint enforcement is that the RESTRICT action processing
- * happens as soon as the field is updated - not at the end of the current statement as it would
- * with an immediate constraint, or at the end of the current transaction as it would with a
- * {@link #deferred()} constraint.
- * <p>
- * Even if the foreign key constraint it is attached to is {@link #deferred()}, configuring a
- * RESTRICT action causes SQLite to return an error immediately if a parent key with dependent
- * child keys is deleted or modified.
- */
- int RESTRICT = 2;
-
- /**
- * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
- * <p>
- * If the configured action is "SET NULL", then when a parent key is deleted
- * (for {@link #onDelete()}) or modified (for {@link #onUpdate()}), the child key columns of all
- * rows in the child table that mapped to the parent key are set to contain {@code NULL} values.
- */
- int SET_NULL = 3;
-
- /**
- * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
- * <p>
- * The "SET DEFAULT" actions are similar to {@link #SET_NULL}, except that each of the child key
- * columns is set to contain the columns default value instead of {@code NULL}.
- */
- int SET_DEFAULT = 4;
-
- /**
- * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
- * <p>
- * A "CASCADE" action propagates the delete or update operation on the parent key to each
- * dependent child key. For {@link #onDelete()} action, this means that each row in the child
- * entity that was associated with the deleted parent row is also deleted. For an
- * {@link #onUpdate()} action, it means that the values stored in each dependent child key are
- * modified to match the new parent key values.
- */
- int CASCADE = 5;
-
- /**
- * Constants definition for values that can be used in {@link #onDelete()} and
- * {@link #onUpdate()}.
- */
- @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
- @interface Action {
- }
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Ignore.java b/room/common/src/main/java/android/arch/persistence/room/Ignore.java
deleted file mode 100644
index 6effc33..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Ignore.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-/**
- * Ignores the marked element from Room's processing logic.
- * <p>
- * This annotation can be used in multiple places where Room processor runs. For instance, you can
- * add it to a field of an {@link Entity} and Room will not persist that field.
- */
-@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
-@Retention(RetentionPolicy.CLASS)
-public @interface Ignore {
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Index.java b/room/common/src/main/java/android/arch/persistence/room/Index.java
deleted file mode 100644
index b814efd..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Index.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Declares an index on an Entity.
- * see: <a href="https://sqlite.org/lang_createindex.html">SQLite Index Documentation</a>
- * <p>
- * Adding an index usually speeds up your select queries but will slow down other queries like
- * insert or update. You should be careful when adding indices to ensure that this additional cost
- * is worth the gain.
- * <p>
- * There are 2 ways to define an index in an {@link Entity}. You can either set
- * {@link ColumnInfo#index()} property to index individual fields or define composite indices via
- * {@link Entity#indices()}.
- * <p>
- * If an indexed field is embedded into another Entity via {@link Embedded}, it is <b>NOT</b>
- * added as an index to the containing {@link Entity}. If you want to keep it indexed, you must
- * re-declare it in the containing {@link Entity}.
- * <p>
- * Similarly, if an {@link Entity} extends another class, indices from the super classes are
- * <b>NOT</b> inherited. You must re-declare them in the child {@link Entity} or set
- * {@link Entity#inheritSuperIndices()} to {@code true}.
- * */
-@Target({})
-@Retention(RetentionPolicy.CLASS)
-public @interface Index {
- /**
- * List of column names in the Index.
- * <p>
- * The order of columns is important as it defines when SQLite can use a particular index.
- * See <a href="https://www.sqlite.org/optoverview.html">SQLite documentation</a> for details on
- * index usage in the query optimizer.
- *
- * @return The list of column names in the Index.
- */
- String[] value();
-
- /**
- * Name of the index. If not set, Room will set it to the list of columns joined by '_' and
- * prefixed by "index_${tableName}". So if you have a table with name "Foo" and with an index
- * of {"bar", "baz"}, generated index name will be "index_Foo_bar_baz". If you need to specify
- * the index in a query, you should never rely on this name, instead, specify a name for your
- * index.
- *
- * @return The name of the index.
- */
- String name() default "";
-
- /**
- * If set to true, this will be a unique index and any duplicates will be rejected.
- *
- * @return True if index is unique. False by default.
- */
- boolean unique() default false;
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Insert.java b/room/common/src/main/java/android/arch/persistence/room/Insert.java
deleted file mode 100644
index 802dd96..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Insert.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 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 a {@link Dao} annotated class as an insert method.
- * <p>
- * The implementation of the method will insert its parameters into the database.
- * <p>
- * All of the parameters of the Insert method must either be classes annotated with {@link Entity}
- * or collections/array of it.
- * <p>
- * Example:
- * <pre>
- * {@literal @}Dao
- * public interface MyDao {
- * {@literal @}Insert(onConflict = OnConflictStrategy.REPLACE)
- * public void insertUsers(User... users);
- * {@literal @}Insert
- * public void insertBoth(User user1, User user2);
- * {@literal @}Insert
- * public void insertWithFriends(User user, List<User> friends);
- * }
- * </pre>
- *
- * @see Update
- * @see Delete
- */
-@Target({ElementType.METHOD})
-@Retention(RetentionPolicy.CLASS)
-public @interface Insert {
- /**
- * What to do if a conflict happens.
- * @see <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a>
- *
- * @return How to handle conflicts. Defaults to {@link OnConflictStrategy#ABORT}.
- */
- @OnConflictStrategy
- int onConflict() default OnConflictStrategy.ABORT;
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/OnConflictStrategy.java b/room/common/src/main/java/android/arch/persistence/room/OnConflictStrategy.java
deleted file mode 100644
index 5217b61..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/OnConflictStrategy.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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 static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.support.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-
-/**
- * Set of conflict handling strategies for various {@link Dao} methods.
- * <p>
- * Check <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a> for
- * details.
- */
-@Retention(SOURCE)
-@IntDef({OnConflictStrategy.REPLACE, OnConflictStrategy.ROLLBACK, OnConflictStrategy.ABORT,
- OnConflictStrategy.FAIL, OnConflictStrategy.IGNORE})
-public @interface OnConflictStrategy {
- /**
- * OnConflict strategy constant to replace the old data and continue the transaction.
- */
- int REPLACE = 1;
- /**
- * OnConflict strategy constant to rollback the transaction.
- */
- int ROLLBACK = 2;
- /**
- * OnConflict strategy constant to abort the transaction.
- */
- int ABORT = 3;
- /**
- * OnConflict strategy constant to fail the transaction.
- */
- int FAIL = 4;
- /**
- * OnConflict strategy constant to ignore the conflict.
- */
- int IGNORE = 5;
-
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/PrimaryKey.java b/room/common/src/main/java/android/arch/persistence/room/PrimaryKey.java
deleted file mode 100644
index 9a8062ca..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/PrimaryKey.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2016 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 field in an {@link Entity} as the primary key.
- * <p>
- * If you would like to define a composite primary key, you should use {@link Entity#primaryKeys()}
- * method.
- * <p>
- * Each {@link Entity} must declare a primary key unless one of its super classes declares a
- * primary key. If both an {@link Entity} and its super class defines a {@code PrimaryKey}, the
- * child's {@code PrimaryKey} definition will override the parent's {@code PrimaryKey}.
- * <p>
- * If {@code PrimaryKey} annotation is used on a {@link Embedded}d field, all columns inherited
- * from that embedded field becomes the composite primary key (including its grand children
- * fields).
- */
-@Target(ElementType.FIELD)
-@Retention(RetentionPolicy.CLASS)
-public @interface PrimaryKey {
- /**
- * Set to true to let SQLite generate the unique id.
- * <p>
- * When set to {@code true}, the SQLite type affinity for the field should be {@code INTEGER}.
- * <p>
- * If the field type is {@code long} or {@code int} (or its TypeConverter converts it to a
- * {@code long} or {@code int}), {@link Insert} methods treat {@code 0} as not-set while
- * inserting the item.
- * <p>
- * If the field's type is {@link Integer} or {@link Long} (or its TypeConverter converts it to
- * an {@link Integer} or a {@link Long}), {@link Insert} methods treat {@code null} as
- * not-set while inserting the item.
- *
- * @return Whether the primary key should be auto-generated by SQLite or not. Defaults
- * to false.
- */
- boolean autoGenerate() default false;
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Query.java b/room/common/src/main/java/android/arch/persistence/room/Query.java
deleted file mode 100644
index bd838e8..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Query.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2016 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 a {@link Dao} annotated class as a query method.
- * <p>
- * The value of the annotation includes the query that will be run when this method is called. This
- * query is <b>verified at compile time</b> by Room to ensure that it compiles fine against the
- * database.
- * <p>
- * The arguments of the method will be bound to the bind arguments in the SQL statement. See
- * <href="https://www.sqlite.org/c3ref/bind_blob.html">SQLite's binding documentation</> for
- * details of bind arguments in SQLite.
- * <p>
- * Room only supports named bind parameter {@code :name} to avoid any confusion between the
- * method parameters and the query bind parameters.
- * <p>
- * Room will automatically bind the parameters of the method into the bind arguments. This is done
- * by matching the name of the parameters to the name of the bind arguments.
- * <pre>
- * {@literal @}Query("SELECT * FROM user WHERE user_name LIKE :name AND last_name LIKE :last")
- * public abstract List<User> findUsersByNameAndLastName(String name, String last);
- * </pre>
- * <p>
- * As an extension over SQLite bind arguments, Room supports binding a list of parameters to the
- * query. At runtime, Room will build the correct query to have matching number of bind arguments
- * depending on the number of items in the method parameter.
- * <pre>
- * {@literal @}Query("SELECT * FROM user WHERE uid IN(:userIds)")
- * public abstract List<User> findByIds(int[] userIds);
- * </pre>
- * For the example above, if the {@code userIds} is an array of 3 elements, Room will run the
- * query as: {@code SELECT * FROM user WHERE uid IN(?, ?, ?)} and bind each item in the
- * {@code userIds} array into the statement.
- * <p>
- * There are 3 types of queries supported in {@code Query} methods: SELECT, UPDATE and DELETE.
- * <p>
- * For SELECT queries, Room will infer the result contents from the method's return type and
- * generate the code that will automatically convert the query result into the method's return
- * type. For single result queries, the return type can be any java object. For queries that return
- * multiple values, you can use {@link java.util.List} or {@code Array}. In addition to these, any
- * query may return {@link android.database.Cursor Cursor} or any query result can be wrapped in
- * a {@link android.arch.lifecycle.LiveData LiveData}.
- * <p>
- * <b>RxJava2</b> If you are using RxJava2, you can also return {@code Flowable<T>} or
- * {@code Publisher<T>} from query methods. Since Reactive Streams does not allow {@code null}, if
- * the query returns a nullable type, it will not dispatch anything if the value is {@code null}
- * (like fetching an {@link Entity} row that does not exist).
- * You can return {@code Flowable<T[]>} or {@code Flowable<List<T>>} to workaround this limitation.
- * <p>
- * Both {@code Flowable<T>} and {@code Publisher<T>} will observe the database for changes and
- * re-dispatch if data changes. If you want to query the database without observing changes, you can
- * use {@code Maybe<T>} or {@code Single<T>}. If a {@code Single<T>} query returns {@code null},
- * Room will throw
- * {@link android.arch.persistence.room.EmptyResultSetException EmptyResultSetException}.
- * <p>
- * UPDATE or DELETE queries can return {@code void} or {@code int}. If it is an {@code int},
- * the value is the number of rows affected by this query.
- * <p>
- * You can return arbitrary POJOs from your query methods as long as the fields of the POJO match
- * the column names in the query result.
- * For example, if you have class:
- * <pre>
- * class UserName {
- * public String name;
- * {@literal @}ColumnInfo(name = "last_name")
- * public String lastName;
- * }
- * </pre>
- * You can write a query like this:
- * <pre>
- * {@literal @}Query("SELECT last_name, name FROM user WHERE uid = :userId LIMIT 1")
- * public abstract UserName findOneUserName(int userId);
- * </pre>
- * And Room will create the correct implementation to convert the query result into a
- * {@code UserName} object. If there is a mismatch between the query result and the fields of the
- * POJO, as long as there is at least 1 field match, Room prints a
- * {@link RoomWarnings#CURSOR_MISMATCH} warning and sets as many fields as it can.
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.CLASS)
-public @interface Query {
- /**
- * The SQLite query to be run.
- * @return The query to be run.
- */
- String value();
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/RawQuery.java b/room/common/src/main/java/android/arch/persistence/room/RawQuery.java
deleted file mode 100644
index 46f2193..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/RawQuery.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2018 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 a {@link Dao} annotated class as a raw query method where you can pass the
- * query as a {@link String} or a
- * {@link android.arch.persistence.db.SupportSQLiteQuery SupportSQLiteQuery}.
- * <pre>
- * {@literal @}Dao
- * interface RawDao {
- * {@literal @}RawQuery
- * User getUser(String query);
- * {@literal @}RawQuery
- * User getUserViaQuery(SupportSQLiteQuery query);
- * }
- * User user = rawDao.getUser("SELECT * FROM User WHERE id = 3 LIMIT 1");
- * SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1",
- * new Object[]{3});
- * User user2 = rawDao.getUserViaQuery(query);
- * </pre>
- * <p>
- * Room will generate the code based on the return type of the function and failure to
- * pass a proper query will result in a runtime failure or an undefined result.
- * <p>
- * If you know the query at compile time, you should always prefer {@link Query} since it validates
- * the query at compile time and also generates more efficient code since Room can compute the
- * query result at compile time (e.g. it does not need to account for possibly missing columns in
- * the response).
- * <p>
- * On the other hand, {@code RawQuery} serves as an escape hatch where you can build your own
- * SQL query at runtime but still use Room to convert it into objects.
- * <p>
- * {@code RawQuery} methods must return a non-void type. If you want to execute a raw query that
- * does not return any value, use {@link android.arch.persistence.room.RoomDatabase#query
- * RoomDatabase#query} methods.
- * <p>
- * <b>Observable Queries:</b>
- * <p>
- * {@code RawQuery} methods can return observable types but you need to specify which tables are
- * accessed in the query using the {@link #observedEntities()} field in the annotation.
- * <pre>
- * {@literal @}Dao
- * interface RawDao {
- * {@literal @}RawQuery(observedEntities = User.class)
- * LiveData<List<User>> getUsers(String query);
- * }
- * LiveData<List<User>> liveUsers = rawDao.getUsers("SELECT * FROM User ORDER BY name DESC");
- * </pre>
- * <b>Returning Pojos:</b>
- * <p>
- * RawQueries can also return plain old java objects, similar to {@link Query} methods.
- * <pre>
- * public class NameAndLastName {
- * public final String name;
- * public final String lastName;
- *
- * public NameAndLastName(String name, String lastName) {
- * this.name = name;
- * this.lastName = lastName;
- * }
- * }
- *
- * {@literal @}Dao
- * interface RawDao {
- * {@literal @}RawQuery
- * NameAndLastName getNameAndLastName(String query);
- * }
- * NameAndLastName result = rawDao.getNameAndLastName("SELECT * FROM User WHERE id = 3")
- * // or
- * NameAndLastName result = rawDao.getNameAndLastName("SELECT name, lastName FROM User WHERE id =
- * 3")
- * </pre>
- * <p>
- * <b>Pojos with Embedded Fields:</b>
- * <p>
- * {@code RawQuery} methods can return pojos that include {@link Embedded} fields as well.
- * <pre>
- * public class UserAndPet {
- * {@literal @}Embedded
- * public User user;
- * {@literal @}Embedded
- * public Pet pet;
- * }
- *
- * {@literal @}Dao
- * interface RawDao {
- * {@literal @}RawQuery
- * UserAndPet getUserAndPet(String query);
- * }
- * UserAndPet received = rawDao.getUserAndPet(
- * "SELECT * FROM User, Pet WHERE User.id = Pet.userId LIMIT 1")
- * </pre>
- *
- * <b>Relations:</b>
- * <p>
- * {@code RawQuery} return types can also be objects with {@link Relation Relations}.
- * <pre>
- * public class UserAndAllPets {
- * {@literal @}Embedded
- * public User user;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
- * public List<Pet> pets;
- * }
- *
- * {@literal @}Dao
- * interface RawDao {
- * {@literal @}RawQuery
- * List<UserAndAllPets> getUsersAndAllPets(String query);
- * }
- * List<UserAndAllPets> result = rawDao.getUsersAndAllPets("SELECT * FROM users");
- * </pre>
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.CLASS)
-public @interface RawQuery {
- /**
- * Denotes the list of entities which are accessed in the provided query and should be observed
- * for invalidation if the query is observable.
- * <p>
- * The listed classes should either be annotated with {@link Entity} or they should reference to
- * at least 1 Entity (via {@link Embedded} or {@link Relation}).
- * <p>
- * Providing this field in a non-observable query has no impact.
- * <pre>
- * {@literal @}Dao
- * interface RawDao {
- * {@literal @}RawQuery(observedEntities = User.class)
- * LiveData<List<User>> getUsers(String query);
- * }
- * LiveData<List<User>> liveUsers = rawDao.getUsers("select * from User ORDER BY name
- * DESC");
- * </pre>
- *
- * @return List of entities that should invalidate the query if changed.
- */
- Class[] observedEntities() default {};
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Relation.java b/room/common/src/main/java/android/arch/persistence/room/Relation.java
deleted file mode 100644
index d55bbfe..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Relation.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * 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;
-
-/**
- * A convenience annotation which can be used in a Pojo to automatically fetch relation entities.
- * When the Pojo is returned from a query, all of its relations are also fetched by Room.
- *
- * <pre>
- * {@literal @}Entity
- * public class Pet {
- * {@literal @} PrimaryKey
- * int id;
- * int userId;
- * String name;
- * // other fields
- * }
- * public class UserNameAndAllPets {
- * public int id;
- * public String name;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
- * public List<Pet> pets;
- * }
- *
- * {@literal @}Dao
- * public interface UserPetDao {
- * {@literal @}Query("SELECT id, name from User")
- * public List<UserNameAndAllPets> loadUserAndPets();
- * }
- * </pre>
- * <p>
- * The type of the field annotated with {@code Relation} must be a {@link java.util.List} or
- * {@link java.util.Set}. By default, the {@link Entity} type is inferred from the return type.
- * If you would like to return a different object, you can specify the {@link #entity()} property
- * in the annotation.
- * <pre>
- * public class User {
- * int id;
- * // other fields
- * }
- * public class PetNameAndId {
- * int id;
- * String name;
- * }
- * public class UserAllPets {
- * {@literal @}Embedded
- * public User user;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class)
- * public List<PetNameAndId> pets;
- * }
- * {@literal @}Dao
- * public interface UserPetDao {
- * {@literal @}Query("SELECT * from User")
- * public List<UserAllPets> loadUserAndPets();
- * }
- * </pre>
- * <p>
- * In the example above, {@code PetNameAndId} is a regular Pojo but all of fields are fetched
- * from the {@code entity} defined in the {@code @Relation} annotation (<i>Pet</i>).
- * {@code PetNameAndId} could also define its own relations all of which would also be fetched
- * automatically.
- * <p>
- * If you would like to specify which columns are fetched from the child {@link Entity}, you can
- * use {@link #projection()} property in the {@code Relation} annotation.
- * <pre>
- * public class UserAndAllPets {
- * {@literal @}Embedded
- * public User user;
- * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class,
- * projection = {"name"})
- * public List<String> petNames;
- * }
- * </pre>
- * <p>
- * Note that {@code @Relation} annotation can be used only in Pojo classes, an {@link Entity} class
- * cannot have relations. This is a design decision to avoid common pitfalls in {@link Entity}
- * setups. You can read more about it in the main Room documentation. When loading data, you can
- * simply work around this limitation by creating Pojo classes that extend the {@link Entity}.
- * <p>
- * Note that the {@code @Relation} annotated field cannot be a constructor parameter, it must be
- * public or have a public setter.
- */
-@Target(ElementType.FIELD)
-@Retention(RetentionPolicy.CLASS)
-public @interface Relation {
- /**
- * The entity to fetch the item from. You don't need to set this if the entity matches the
- * type argument in the return type.
- *
- * @return The entity to fetch from. By default, inherited from the return type.
- */
- Class entity() default Object.class;
-
- /**
- * Reference field in the parent Pojo.
- * <p>
- * If you would like to access to a sub item of a {@link Embedded}d field, you can use
- * the {@code .} notation.
- * <p>
- * For instance, if you have a {@link Embedded}d field named {@code user} with a sub field
- * {@code id}, you can reference it via {@code user.id}.
- * <p>
- * This value will be matched against the value defined in {@link #entityColumn()}.
- *
- * @return The field reference in the parent object.
- */
- String parentColumn();
-
- /**
- * The field path to match in the {@link #entity()}. This value will be matched against the
- * value defined in {@link #parentColumn()}.
- */
- String entityColumn();
-
- /**
- * If sub fields should be fetched from the entity, you can specify them using this field.
- * <p>
- * By default, inferred from the the return type.
- *
- * @return The list of columns to be selected from the {@link #entity()}.
- */
- String[] projection() default {};
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/RoomMasterTable.java b/room/common/src/main/java/android/arch/persistence/room/RoomMasterTable.java
deleted file mode 100644
index 621845d..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/RoomMasterTable.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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 android.support.annotation.RestrictTo;
-
-/**
- * Schema information about Room's master table.
- *
- * @hide
- */
-@SuppressWarnings("WeakerAccess")
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class RoomMasterTable {
- /**
- * The master table where room keeps its metadata information.
- */
- public static final String TABLE_NAME = "room_master_table";
- // must match the runtime property Room#MASTER_TABLE_NAME
- public static final String NAME = "room_master_table";
- private static final String COLUMN_ID = "id";
- private static final String COLUMN_IDENTITY_HASH = "identity_hash";
- public static final String DEFAULT_ID = "42";
-
- public static final String CREATE_QUERY = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
- + COLUMN_ID + " INTEGER PRIMARY KEY,"
- + COLUMN_IDENTITY_HASH + " TEXT)";
-
- public static final String READ_QUERY = "SELECT " + COLUMN_IDENTITY_HASH
- + " FROM " + TABLE_NAME + " WHERE "
- + COLUMN_ID + " = " + DEFAULT_ID + " LIMIT 1";
-
- /**
- * We don't escape here since we know what we are passing.
- */
- public static String createInsertQuery(String hash) {
- return "INSERT OR REPLACE INTO " + TABLE_NAME + " ("
- + COLUMN_ID + "," + COLUMN_IDENTITY_HASH + ")"
- + " VALUES(" + DEFAULT_ID + ", \"" + hash + "\")";
- }
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
deleted file mode 100644
index f05e6be..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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;
-
-/**
- * The list of warnings that are produced by Room.
- * <p>
- * You can use these values inside a {@link SuppressWarnings} annotation to disable the warnings.
- */
-@SuppressWarnings({"unused", "WeakerAccess"})
-public class RoomWarnings {
- /**
- * The warning dispatched by Room when the return value of a {@link Query} method does not
- * exactly match the fields in the query result.
- */
- // if you change this, don't forget to change android.arch.persistence.room.vo.Warning
- public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
-
- /**
- * Reported when Room cannot verify database queries during compilation due to lack of
- * tmp dir access in JVM.
- */
- public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
-
- /**
- * Reported when Room cannot verify database queries during compilation. This usually happens
- * when it cannot find the SQLite JDBC driver on the host machine.
- * <p>
- * Room can function without query verification but its functionality will be limited.
- */
- public static final String CANNOT_CREATE_VERIFICATION_DATABASE =
- "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
-
- /**
- * Reported when an {@link Entity} field that is annotated with {@link Embedded} has a
- * sub field which is annotated with {@link PrimaryKey} but the {@link PrimaryKey} is dropped
- * while composing it into the parent object.
- */
- public static final String PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED =
- "ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED";
-
- /**
- * Reported when an {@link Entity} field that is annotated with {@link Embedded} has a
- * sub field which has a {@link ColumnInfo} annotation with {@code index = true}.
- * <p>
- * You can re-define the index in the containing {@link Entity}.
- */
- public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED =
- "ROOM_EMBEDDED_INDEX_IS_DROPPED";
-
- /**
- * Reported when an {@link Entity} that has a {@link Embedded}d field whose type is another
- * {@link Entity} and that {@link Entity} has some indices defined.
- * These indices will NOT be created in the containing {@link Entity}. If you want to preserve
- * them, you can re-define them in the containing {@link Entity}.
- */
- public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED =
- "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
-
- /**
- * Reported when an {@link Entity}'s parent declares an {@link Index}. Room does not
- * automatically inherit these indices to avoid hidden costs or unexpected constraints.
- * <p>
- * If you want your child class to have the indices of the parent, you must re-declare
- * them in the child class. Alternatively, you can set {@link Entity#inheritSuperIndices()}
- * to {@code true}.
- */
- public static final String INDEX_FROM_PARENT_IS_DROPPED =
- "ROOM_PARENT_INDEX_IS_DROPPED";
-
- /**
- * Reported when an {@link Entity} inherits a field from its super class and the field has a
- * {@link ColumnInfo} annotation with {@code index = true}.
- * <p>
- * These indices are dropped for the {@link Entity} and you would need to re-declare them if
- * you want to keep them. Alternatively, you can set {@link Entity#inheritSuperIndices()}
- * to {@code true}.
- */
- public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED =
- "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
-
- /**
- * Reported when a {@link Relation} {@link Entity}'s SQLite column type does not match the type
- * in the parent. Room will still do the matching using {@code String} representations.
- */
- public static final String RELATION_TYPE_MISMATCH = "ROOM_RELATION_TYPE_MISMATCH";
-
- /**
- * Reported when a `room.schemaLocation` argument is not provided into the annotation processor.
- * You can either set {@link Database#exportSchema()} to {@code false} or provide
- * `room.schemaLocation` to the annotation processor. You are strongly adviced to provide it
- * and also commit them into your version control system.
- */
- public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
-
- /**
- * When there is a foreign key from Entity A to Entity B, it is a good idea to index the
- * reference columns in B, otherwise, each modification on Entity A will trigger a full table
- * scan on Entity B.
- * <p>
- * If Room cannot find a proper index in the child entity (Entity B in this case), Room will
- * print this warning.
- */
- 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";
-
- /**
- * Reported when a @Query method returns a Pojo that has relations but the method is not
- * annotated with @Transaction. Relations are run as separate queries and if the query is not
- * run inside a transaction, it might return inconsistent results from the database.
- */
- public static final String RELATION_QUERY_WITHOUT_TRANSACTION =
- "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/SkipQueryVerification.java b/room/common/src/main/java/android/arch/persistence/room/SkipQueryVerification.java
deleted file mode 100644
index dc2ded3..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/SkipQueryVerification.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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;
-
-/**
- * Skips database verification for the annotated element.
- * <p>
- * If it is a class annotated with {@link Database}, none of the queries for the database will
- * be verified at compile time.
- * <p>
- * If it is a class annotated with {@link Dao}, none of the queries in the Dao class will
- * be verified at compile time.
- * <p>
- * If it is a method in a Dao class, just the method's sql verification will be skipped.
- * <p>
- * You should use this as the last resort if Room cannot properly understand your query and you are
- * 100% sure it works. Removing validation may limit the functionality of Room since it won't be
- * able to understand the query response.
- */
-@Target({ElementType.METHOD, ElementType.TYPE})
-@Retention(RetentionPolicy.CLASS)
-public @interface SkipQueryVerification {
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Transaction.java b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
deleted file mode 100644
index 3b6ede9..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Transaction.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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 a {@link Dao} class as a transaction method.
- * <p>
- * When used on a non-abstract method of an abstract {@link Dao} class,
- * 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>
- * <p>
- * When used on a {@link Query} method that has a {@code Select} statement, the generated code for
- * the Query will be run in a transaction. There are 2 main cases where you may want to do that:
- * <ol>
- * <li>If the result of the query is fairly big, it is better to run it inside a transaction
- * to receive a consistent result. Otherwise, if the query result does not fit into a single
- * {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to
- * changes in the database in between cursor window swaps.
- * <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are
- * queried separately. To receive consistent results between these queries, you probably want
- * to run them in a single transaction.
- * </ol>
- * Example:
- * <pre>
- * class ProductWithReviews extends Product {
- * {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class)
- * public List<Review> reviews;
- * }
- * {@literal @}Dao
- * public interface ProductDao {
- * {@literal @}Transaction {@literal @}Query("SELECT * from products")
- * public List<ProductWithReviews> loadAll();
- * }
- * </pre>
- * If the query is an async query (e.g. returns a {@link android.arch.lifecycle.LiveData LiveData}
- * or RxJava Flowable, the transaction is properly handled when the query is run, not when the
- * method is called.
- * <p>
- * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no
- * impact because they are always run inside a transaction. Similarly, if it is annotated with
- * {@link Query} but runs an update or delete statement, it is automatically wrapped in a
- * transaction.
- */
-@Target({ElementType.METHOD})
-@Retention(RetentionPolicy.CLASS)
-public @interface Transaction {
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/TypeConverter.java b/room/common/src/main/java/android/arch/persistence/room/TypeConverter.java
deleted file mode 100644
index 850481c..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/TypeConverter.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 as a type converter. A class can have as many @TypeConverter methods as it needs.
- * <p>
- * Each converter method should receive 1 parameter and have non-void return type.
- *
- * <pre>
- * // example converter for java.util.Date
- * public static class Converters {
- * {@literal @}TypeConverter
- * public Date fromTimestamp(Long value) {
- * return value == null ? null : new Date(value);
- * }
- *
- * {@literal @}TypeConverter
- * public Long dateToTimestamp(Date date) {
- * if (date == null) {
- * return null;
- * } else {
- * return date.getTime();
- * }
- * }
- *}
- * </pre>
- * @see TypeConverters
- */
-@Target({ElementType.METHOD})
-@Retention(RetentionPolicy.CLASS)
-public @interface TypeConverter {
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/TypeConverters.java b/room/common/src/main/java/android/arch/persistence/room/TypeConverters.java
deleted file mode 100644
index 7f7e6ab..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/TypeConverters.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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;
-
-/**
- * Specifies additional type converters that Room can use. The TypeConverter is added to the scope
- * of the element so if you put it on a class / interface, all methods / fields in that class will
- * be able to use the converters.
- * <ul>
- * <li>If you put it on a {@link Database}, all Daos and Entities in that database will be able to
- * use it.
- * <li>If you put it on a {@link Dao}, all methods in the Dao will be able to use it.
- * <li>If you put it on an {@link Entity}, all fields of the Entity will be able to use it.
- * <li>If you put it on a POJO, all fields of the POJO will be able to use it.
- * <li>If you put it on an {@link Entity} field, only that field will be able to use it.
- * <li>If you put it on a {@link Dao} method, all parameters of the method will be able to use it.
- * <li>If you put it on a {@link Dao} method parameter, just that field will be able to use it.
- * </ul>
- * @see TypeConverter
- */
-@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.FIELD})
-@Retention(RetentionPolicy.CLASS)
-public @interface TypeConverters {
- /**
- * The list of type converter classes. If converter methods are not static, Room will create
- * an instance of these classes.
- *
- * @return The list of classes that contains the converter methods.
- */
- Class<?>[] value();
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/Update.java b/room/common/src/main/java/android/arch/persistence/room/Update.java
deleted file mode 100644
index 670a085..0000000
--- a/room/common/src/main/java/android/arch/persistence/room/Update.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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;
-
-/**
- * Marks a method in a {@link Dao} annotated class as an update method.
- * <p>
- * The implementation of the method will update its parameters in the database if they already
- * exists (checked by primary keys). If they don't already exists, this option will not change the
- * database.
- * <p>
- * All of the parameters of the Update method must either be classes annotated with {@link Entity}
- * or collections/array of it.
- *
- * @see Insert
- * @see Delete
- */
-public @interface Update {
- /**
- * What to do if a conflict happens.
- * @see <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a>
- *
- * @return How to handle conflicts. Defaults to {@link OnConflictStrategy#ABORT}.
- */
- @OnConflictStrategy
- int onConflict() default OnConflictStrategy.ABORT;
-}
diff --git a/room/common/src/main/java/androidx/room/ColumnInfo.java b/room/common/src/main/java/androidx/room/ColumnInfo.java
new file mode 100644
index 0000000..404f334
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/ColumnInfo.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresApi;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Allows specific customization about the column associated with this field.
+ * <p>
+ * For example, you can specify a column name for the field or change the column's type affinity.
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.CLASS)
+public @interface ColumnInfo {
+ /**
+ * Name of the column in the database. Defaults to the field name if not set.
+ *
+ * @return Name of the column in the database.
+ */
+ String name() default INHERIT_FIELD_NAME;
+
+ /**
+ * The type affinity for the column, which will be used when constructing the database.
+ * <p>
+ * If it is not specified, the value defaults to {@link #UNDEFINED} and Room resolves it based
+ * on the field's type and available TypeConverters.
+ * <p>
+ * See <a href="https://www.sqlite.org/datatype3.html">SQLite types documentation</a> for
+ * details.
+ *
+ * @return The type affinity of the column. This is either {@link #UNDEFINED}, {@link #TEXT},
+ * {@link #INTEGER}, {@link #REAL}, or {@link #BLOB}.
+ */
+ @SuppressWarnings("unused") @SQLiteTypeAffinity int typeAffinity() default UNDEFINED;
+
+ /**
+ * Convenience method to index the field.
+ * <p>
+ * If you would like to create a composite index instead, see: {@link Index}.
+ *
+ * @return True if this field should be indexed, false otherwise. Defaults to false.
+ */
+ boolean index() default false;
+
+ /**
+ * The collation sequence for the column, which will be used when constructing the database.
+ * <p>
+ * The default value is {@link #UNSPECIFIED}. In that case, Room does not add any
+ * collation sequence to the column, and SQLite treats it like {@link #BINARY}.
+ *
+ * @return The collation sequence of the column. This is either {@link #UNSPECIFIED},
+ * {@link #BINARY}, {@link #NOCASE}, {@link #RTRIM}, {@link #LOCALIZED} or {@link #UNICODE}.
+ */
+ @Collate int collate() default UNSPECIFIED;
+
+ /**
+ * Constant to let Room inherit the field name as the column name. If used, Room will use the
+ * field name as the column name.
+ */
+ String INHERIT_FIELD_NAME = "[field-name]";
+
+ /**
+ * Undefined type affinity. Will be resolved based on the type.
+ *
+ * @see #typeAffinity()
+ */
+ int UNDEFINED = 1;
+ /**
+ * Column affinity constant for strings.
+ *
+ * @see #typeAffinity()
+ */
+ int TEXT = 2;
+ /**
+ * Column affinity constant for integers or booleans.
+ *
+ * @see #typeAffinity()
+ */
+ int INTEGER = 3;
+ /**
+ * Column affinity constant for floats or doubles.
+ *
+ * @see #typeAffinity()
+ */
+ int REAL = 4;
+ /**
+ * Column affinity constant for binary data.
+ *
+ * @see #typeAffinity()
+ */
+ int BLOB = 5;
+
+ /**
+ * The SQLite column type constants that can be used in {@link #typeAffinity()}
+ */
+ @IntDef({UNDEFINED, TEXT, INTEGER, REAL, BLOB})
+ @interface SQLiteTypeAffinity {
+ }
+
+ /**
+ * Collation sequence is not specified. The match will behave like {@link #BINARY}.
+ *
+ * @see #collate()
+ */
+ int UNSPECIFIED = 1;
+ /**
+ * Collation sequence for case-sensitive match.
+ *
+ * @see #collate()
+ */
+ int BINARY = 2;
+ /**
+ * Collation sequence for case-insensitive match.
+ *
+ * @see #collate()
+ */
+ int NOCASE = 3;
+ /**
+ * Collation sequence for case-sensitive match except that trailing space characters are
+ * ignored.
+ *
+ * @see #collate()
+ */
+ int RTRIM = 4;
+ /**
+ * Collation sequence that uses system's current locale.
+ *
+ * @see #collate()
+ */
+ @RequiresApi(21)
+ int LOCALIZED = 5;
+ /**
+ * Collation sequence that uses Unicode Collation Algorithm.
+ *
+ * @see #collate()
+ */
+ @RequiresApi(21)
+ int UNICODE = 6;
+
+ @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
+ @interface Collate {
+ }
+}
diff --git a/room/common/src/main/java/androidx/room/Dao.java b/room/common/src/main/java/androidx/room/Dao.java
new file mode 100644
index 0000000..c62c641
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Dao.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks the class as a Data Access Object.
+ * <p>
+ * Data Access Objects are the main classes where you define your database interactions. They can
+ * include a variety of query methods.
+ * <p>
+ * The class marked with {@code @Dao} should either be an interface or an abstract class. At compile
+ * time, Room will generate an implementation of this class when it is referenced by a
+ * {@link Database}.
+ * <p>
+ * An abstract {@code @Dao} class can optionally have a constructor that takes a {@link Database}
+ * as its only parameter.
+ * <p>
+ * It is recommended to have multiple {@code Dao} classes in your codebase depending on the tables
+ * they touch.
+ *
+ * @see Query
+ * @see Delete
+ * @see Insert
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface Dao {
+}
diff --git a/room/common/src/main/java/androidx/room/Database.java b/room/common/src/main/java/androidx/room/Database.java
new file mode 100644
index 0000000..40058cd
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Database.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class as a RoomDatabase.
+ * <p>
+ * The class should be an abstract class and extend
+ * {@link androidx.room.RoomDatabase RoomDatabase}.
+ * <p>
+ * You can receive an implementation of the class via
+ * {@link androidx.room.Room#databaseBuilder Room.databaseBuilder} or
+ * {@link androidx.room.Room#inMemoryDatabaseBuilder Room.inMemoryDatabaseBuilder}.
+ * <p>
+ * <pre>
+ * // User and Book are classes annotated with {@literal @}Entity.
+ * {@literal @}Database(version = 1, entities = {User.class, Book.class})
+ * abstract class AppDatabase extends RoomDatabase {
+ * // BookDao is a class annotated with {@literal @}Dao.
+ * abstract public BookDao bookDao();
+ * // UserDao is a class annotated with {@literal @}Dao.
+ * abstract public UserDao userDao();
+ * // UserBookDao is a class annotated with {@literal @}Dao.
+ * abstract public UserBookDao userBookDao();
+ * }
+ * </pre>
+ * The example above defines a class that has 2 tables and 3 DAO classes that are used to access it.
+ * There is no limit on the number of {@link Entity} or {@link Dao} classes but they must be unique
+ * within the Database.
+ * <p>
+ * Instead of running queries on the database directly, you are highly recommended to create
+ * {@link Dao} classes. Using Dao classes will allow you to abstract the database communication in
+ * a more logical layer which will be much easier to mock in tests (compared to running direct
+ * sql queries). It also automatically does the conversion from {@code Cursor} to your application
+ * classes so you don't need to deal with lower level database APIs for most of your data access.
+ * <p>
+ * Room also verifies all of your queries in {@link Dao} classes while the application is being
+ * compiled so that if there is a problem in one of the queries, you will be notified instantly.
+ * @see Dao
+ * @see Entity
+ * @see androidx.room.RoomDatabase RoomDatabase
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface Database {
+ /**
+ * The list of entities included in the database. Each entity turns into a table in the
+ * database.
+ *
+ * @return The list of entities in the database.
+ */
+ Class[] entities();
+
+ /**
+ * The database version.
+ *
+ * @return The database version.
+ */
+ int version();
+
+ /**
+ * You can set annotation processor argument ({@code room.schemaLocation})
+ * to tell Room to export the schema into a folder. Even though it is not mandatory, it is a
+ * good practice to have version history in your codebase and you should commit that file into
+ * your version control system (but don't ship it with your app!).
+ * <p>
+ * When {@code room.schemaLocation} is set, Room will check this variable and if it is set to
+ * {@code true}, its schema will be exported into the given folder.
+ * <p>
+ * {@code exportSchema} is {@code true} by default but you can disable it for databases when
+ * you don't want to keep history of versions (like an in-memory only database).
+ *
+ * @return Whether the schema should be exported to the given folder when the
+ * {@code room.schemaLocation} argument is set. Defaults to {@code true}.
+ */
+ boolean exportSchema() default true;
+}
diff --git a/room/common/src/main/java/androidx/room/Delete.java b/room/common/src/main/java/androidx/room/Delete.java
new file mode 100644
index 0000000..7ca63ad
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Delete.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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 androidx.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 a {@link Dao} annotated class as a delete method.
+ * <p>
+ * The implementation of the method will delete its parameters from the database.
+ * <p>
+ * All of the parameters of the Delete method must either be classes annotated with {@link Entity}
+ * or collections/array of it.
+ * <p>
+ * Example:
+ * <pre>
+ * {@literal @}Dao
+ * public interface MyDao {
+ * {@literal @}Delete
+ * public void deleteUsers(User... users);
+ * {@literal @}Delete
+ * public void deleteAll(User user1, User user2);
+ * {@literal @}Delete
+ * public void deleteWithFriends(User user, List<User> friends);
+ * }
+ * </pre>
+ *
+ * @see Insert
+ * @see Query
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface Delete {
+}
diff --git a/room/common/src/main/java/androidx/room/Embedded.java b/room/common/src/main/java/androidx/room/Embedded.java
new file mode 100644
index 0000000..8890c93
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Embedded.java
@@ -0,0 +1,91 @@
+/*
+ * 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Can be used as an annotation on a field of an {@link Entity} or {@code Pojo} to signal that
+ * nested fields (i.e. fields of the annotated field's class) can be referenced directly in the SQL
+ * queries.
+ * <p>
+ * If the container is an {@link Entity}, these sub fields will be columns in the {@link Entity}'s
+ * database table.
+ * <p>
+ * For example, if you have 2 classes:
+ * <pre>
+ * public class Coordinates {
+ * double latitude;
+ * double longitude;
+ * }
+ * public class Address {
+ * String street;
+ * {@literal @}Embedded
+ * Coordinates coordinates;
+ * }
+ * </pre>
+ * Room will consider {@code latitude} and {@code longitude} as if they are fields of the
+ * {@code Address} class when mapping an SQLite row to {@code Address}.
+ * <p>
+ * So if you have a query that returns {@code street, latitude, longitude}, Room will properly
+ * construct an {@code Address} class.
+ * <p>
+ * If the {@code Address} class is annotated with {@link Entity}, its database table will have 3
+ * columns: {@code street, latitude, longitude}
+ * <p>
+ * If there is a name conflict with the fields of the sub object and the owner object, you can
+ * specify a {@link #prefix()} for the items of the sub object. Note that prefix is always applied
+ * to sub fields even if they have a {@link ColumnInfo} with a specific {@code name}.
+ * <p>
+ * If sub fields of an embedded field has {@link PrimaryKey} annotation, they <b>will not</b> be
+ * considered as primary keys in the owner {@link Entity}.
+ * <p>
+ * When an embedded field is read, if all fields of the embedded field (and its sub fields) are
+ * {@code null} in the {@link android.database.Cursor Cursor}, it is set to {@code null}. Otherwise,
+ * it is constructed.
+ * <p>
+ * Note that even if you have {@link TypeConverter}s that convert a {@code null} column into a
+ * {@code non-null} value, if all columns of the embedded field in the
+ * {@link android.database.Cursor Cursor} are null, the {@link TypeConverter} will never be called
+ * and the embedded field will not be constructed.
+ * <p>
+ * You can override this behavior by annotating the embedded field with
+ * {@link androidx.annotation.NonNull}.
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.CLASS)
+public @interface Embedded {
+ /**
+ * Specifies a prefix to prepend the column names of the fields in the embedded fields.
+ * <p>
+ * For the example above, if we've written:
+ * <pre>
+ * {@literal @}Embedded(prefix = "foo_")
+ * Coordinates coordinates;
+ * </pre>
+ * The column names for {@code latitude} and {@code longitude} will be {@code foo_latitude} and
+ * {@code foo_longitude} respectively.
+ * <p>
+ * By default, prefix is the empty string.
+ *
+ * @return The prefix to be used for the fields of the embedded item.
+ */
+ String prefix() default "";
+}
diff --git a/room/common/src/main/java/androidx/room/Entity.java b/room/common/src/main/java/androidx/room/Entity.java
new file mode 100644
index 0000000..fc69ada
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Entity.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class as an entity. This class will have a mapping SQLite table in the database.
+ * <p>
+ * Each entity must have at least 1 field annotated with {@link PrimaryKey}.
+ * You can also use {@link #primaryKeys()} attribute to define the primary key.
+ * <p>
+ * Each entity must either have a no-arg constructor or a constructor whose parameters match
+ * fields (based on type and name). Constructor does not have to receive all fields as parameters
+ * but if a field is not passed into the constructor, it should either be public or have a public
+ * setter. If a matching constructor is available, Room will always use it. If you don't want it
+ * to use a constructor, you can annotate it with {@link Ignore}.
+ * <p>
+ * 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
+ * public class User {
+ * {@literal @}PrimaryKey
+ * private final int uid;
+ * private String name;
+ * {@literal @}ColumnInfo(name = "last_name")
+ * private String lastName;
+ *
+ * public User(int uid) {
+ * this.uid = uid;
+ * }
+ * public String getLastName() {
+ * return lastName;
+ * }
+ * public void setLastName(String lastName) {
+ * this.lastName = lastName;
+ * }
+ * }
+ * </pre>
+ *
+ * @see Dao
+ * @see Database
+ * @see PrimaryKey
+ * @see ColumnInfo
+ * @see Index
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface Entity {
+ /**
+ * The table name in the SQLite database. If not set, defaults to the class name.
+ *
+ * @return The SQLite tableName of the Entity.
+ */
+ String tableName() default "";
+
+ /**
+ * List of indices on the table.
+ *
+ * @return The list of indices on the table.
+ */
+ Index[] indices() default {};
+
+ /**
+ * If set to {@code true}, any Index defined in parent classes of this class will be carried
+ * over to the current {@code Entity}. Note that if you set this to {@code true}, even if the
+ * {@code Entity} has a parent which sets this value to {@code false}, the {@code Entity} will
+ * still inherit indices from it and its parents.
+ * <p>
+ * When the {@code Entity} inherits an index from the parent, it is <b>always</b> renamed with
+ * the default naming schema since SQLite <b>does not</b> allow using the same index name in
+ * multiple tables. See {@link Index} for the details of the default name.
+ * <p>
+ * By default, indices defined in parent classes are dropped to avoid unexpected indices.
+ * When this happens, you will receive a {@link RoomWarnings#INDEX_FROM_PARENT_FIELD_IS_DROPPED}
+ * or {@link RoomWarnings#INDEX_FROM_PARENT_IS_DROPPED} warning during compilation.
+ *
+ * @return True if indices from parent classes should be automatically inherited by this Entity,
+ * false otherwise. Defaults to false.
+ */
+ boolean inheritSuperIndices() default false;
+
+ /**
+ * The list of Primary Key column names.
+ * <p>
+ * If you would like to define an auto generated primary key, you can use {@link PrimaryKey}
+ * annotation on the field with {@link PrimaryKey#autoGenerate()} set to {@code true}.
+ *
+ * @return The primary key of this Entity. Can be empty if the class has a field annotated
+ * with {@link PrimaryKey}.
+ */
+ String[] primaryKeys() default {};
+
+ /**
+ * List of {@link ForeignKey} constraints on this entity.
+ *
+ * @return The list of {@link ForeignKey} constraints on this entity.
+ */
+ ForeignKey[] foreignKeys() default {};
+}
diff --git a/room/common/src/main/java/androidx/room/ForeignKey.java b/room/common/src/main/java/androidx/room/ForeignKey.java
new file mode 100644
index 0000000..847941a
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/ForeignKey.java
@@ -0,0 +1,166 @@
+/*
+ * 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 androidx.room;
+
+import androidx.annotation.IntDef;
+
+/**
+ * Declares a foreign key on another {@link Entity}.
+ * <p>
+ * Foreign keys allows you to specify constraints across Entities such that SQLite will ensure that
+ * the relationship is valid when you modify the database.
+ * <p>
+ * When a foreign key constraint is specified, SQLite requires the referenced columns to be part of
+ * a unique index in the parent table or the primary key of that table. You must create a unique
+ * index in the parent entity that covers the referenced columns (Room will verify this at compile
+ * time and print an error if it is missing).
+ * <p>
+ * It is also recommended to create an index on the child table to avoid full table scans when the
+ * parent table is modified. If a suitable index on the child table is missing, Room will print
+ * {@link RoomWarnings#MISSING_INDEX_ON_FOREIGN_KEY_CHILD} warning.
+ * <p>
+ * A foreign key constraint can be deferred until the transaction is complete. This is useful if
+ * you are doing bulk inserts into the database in a single transaction. By default, foreign key
+ * constraints are immediate but you can change this value by setting {@link #deferred()} to
+ * {@code true}. You can also use
+ * <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>
+ * documentation for details.
+ */
+public @interface ForeignKey {
+ /**
+ * The parent Entity to reference. It must be a class annotated with {@link Entity} and
+ * referenced in the same database.
+ *
+ * @return The parent Entity.
+ */
+ Class entity();
+
+ /**
+ * The list of column names in the parent {@link Entity}.
+ * <p>
+ * Number of columns must match the number of columns specified in {@link #childColumns()}.
+ *
+ * @return The list of column names in the parent Entity.
+ * @see #childColumns()
+ */
+ String[] parentColumns();
+
+ /**
+ * The list of column names in the current {@link Entity}.
+ * <p>
+ * Number of columns must match the number of columns specified in {@link #parentColumns()}.
+ *
+ * @return The list of column names in the current Entity.
+ */
+ String[] childColumns();
+
+ /**
+ * Action to take when the parent {@link Entity} is deleted from the database.
+ * <p>
+ * By default, {@link #NO_ACTION} is used.
+ *
+ * @return The action to take when the referenced entity is deleted from the database.
+ */
+ @Action int onDelete() default NO_ACTION;
+
+ /**
+ * Action to take when the parent {@link Entity} is updated in the database.
+ * <p>
+ * By default, {@link #NO_ACTION} is used.
+ *
+ * @return The action to take when the referenced entity is updated in the database.
+ */
+ @Action int onUpdate() default NO_ACTION;
+
+ /**
+ * * A foreign key constraint can be deferred until the transaction is complete. This is useful
+ * if you are doing bulk inserts into the database in a single transaction. By default, foreign
+ * key constraints are immediate but you can change it by setting this field to {@code true}.
+ * You can also use
+ * <a href="https://sqlite.org/pragma.html#pragma_defer_foreign_keys">defer_foreign_keys</a>
+ * PRAGMA to defer them depending on your transaction.
+ *
+ * @return Whether the foreign key constraint should be deferred until the transaction is
+ * complete. Defaults to {@code false}.
+ */
+ boolean deferred() default false;
+
+ /**
+ * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
+ * <p>
+ * When a parent key is modified or deleted from the database, no special action is taken.
+ * This means that SQLite will not make any effort to fix the constraint failure, instead,
+ * reject the change.
+ */
+ int NO_ACTION = 1;
+
+ /**
+ * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
+ * <p>
+ * The RESTRICT action means that the application is prohibited from deleting
+ * (for {@link #onDelete()}) or modifying (for {@link #onUpdate()}) a parent key when there
+ * exists one or more child keys mapped to it. The difference between the effect of a RESTRICT
+ * action and normal foreign key constraint enforcement is that the RESTRICT action processing
+ * happens as soon as the field is updated - not at the end of the current statement as it would
+ * with an immediate constraint, or at the end of the current transaction as it would with a
+ * {@link #deferred()} constraint.
+ * <p>
+ * Even if the foreign key constraint it is attached to is {@link #deferred()}, configuring a
+ * RESTRICT action causes SQLite to return an error immediately if a parent key with dependent
+ * child keys is deleted or modified.
+ */
+ int RESTRICT = 2;
+
+ /**
+ * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
+ * <p>
+ * If the configured action is "SET NULL", then when a parent key is deleted
+ * (for {@link #onDelete()}) or modified (for {@link #onUpdate()}), the child key columns of all
+ * rows in the child table that mapped to the parent key are set to contain {@code NULL} values.
+ */
+ int SET_NULL = 3;
+
+ /**
+ * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
+ * <p>
+ * The "SET DEFAULT" actions are similar to {@link #SET_NULL}, except that each of the child key
+ * columns is set to contain the columns default value instead of {@code NULL}.
+ */
+ int SET_DEFAULT = 4;
+
+ /**
+ * Possible value for {@link #onDelete()} or {@link #onUpdate()}.
+ * <p>
+ * A "CASCADE" action propagates the delete or update operation on the parent key to each
+ * dependent child key. For {@link #onDelete()} action, this means that each row in the child
+ * entity that was associated with the deleted parent row is also deleted. For an
+ * {@link #onUpdate()} action, it means that the values stored in each dependent child key are
+ * modified to match the new parent key values.
+ */
+ int CASCADE = 5;
+
+ /**
+ * Constants definition for values that can be used in {@link #onDelete()} and
+ * {@link #onUpdate()}.
+ */
+ @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
+ @interface Action {
+ }
+}
diff --git a/room/common/src/main/java/androidx/room/Ignore.java b/room/common/src/main/java/androidx/room/Ignore.java
new file mode 100644
index 0000000..011b949
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Ignore.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Ignores the marked element from Room's processing logic.
+ * <p>
+ * This annotation can be used in multiple places where Room processor runs. For instance, you can
+ * add it to a field of an {@link Entity} and Room will not persist that field.
+ */
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.CLASS)
+public @interface Ignore {
+}
diff --git a/room/common/src/main/java/androidx/room/Index.java b/room/common/src/main/java/androidx/room/Index.java
new file mode 100644
index 0000000..0e30145
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Index.java
@@ -0,0 +1,74 @@
+/*
+ * 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 androidx.room;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares an index on an Entity.
+ * see: <a href="https://sqlite.org/lang_createindex.html">SQLite Index Documentation</a>
+ * <p>
+ * Adding an index usually speeds up your select queries but will slow down other queries like
+ * insert or update. You should be careful when adding indices to ensure that this additional cost
+ * is worth the gain.
+ * <p>
+ * There are 2 ways to define an index in an {@link Entity}. You can either set
+ * {@link ColumnInfo#index()} property to index individual fields or define composite indices via
+ * {@link Entity#indices()}.
+ * <p>
+ * If an indexed field is embedded into another Entity via {@link Embedded}, it is <b>NOT</b>
+ * added as an index to the containing {@link Entity}. If you want to keep it indexed, you must
+ * re-declare it in the containing {@link Entity}.
+ * <p>
+ * Similarly, if an {@link Entity} extends another class, indices from the super classes are
+ * <b>NOT</b> inherited. You must re-declare them in the child {@link Entity} or set
+ * {@link Entity#inheritSuperIndices()} to {@code true}.
+ * */
+@Target({})
+@Retention(RetentionPolicy.CLASS)
+public @interface Index {
+ /**
+ * List of column names in the Index.
+ * <p>
+ * The order of columns is important as it defines when SQLite can use a particular index.
+ * See <a href="https://www.sqlite.org/optoverview.html">SQLite documentation</a> for details on
+ * index usage in the query optimizer.
+ *
+ * @return The list of column names in the Index.
+ */
+ String[] value();
+
+ /**
+ * Name of the index. If not set, Room will set it to the list of columns joined by '_' and
+ * prefixed by "index_${tableName}". So if you have a table with name "Foo" and with an index
+ * of {"bar", "baz"}, generated index name will be "index_Foo_bar_baz". If you need to specify
+ * the index in a query, you should never rely on this name, instead, specify a name for your
+ * index.
+ *
+ * @return The name of the index.
+ */
+ String name() default "";
+
+ /**
+ * If set to true, this will be a unique index and any duplicates will be rejected.
+ *
+ * @return True if index is unique. False by default.
+ */
+ boolean unique() default false;
+}
diff --git a/room/common/src/main/java/androidx/room/Insert.java b/room/common/src/main/java/androidx/room/Insert.java
new file mode 100644
index 0000000..b8378fb
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Insert.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 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 androidx.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 a {@link Dao} annotated class as an insert method.
+ * <p>
+ * The implementation of the method will insert its parameters into the database.
+ * <p>
+ * All of the parameters of the Insert method must either be classes annotated with {@link Entity}
+ * or collections/array of it.
+ * <p>
+ * Example:
+ * <pre>
+ * {@literal @}Dao
+ * public interface MyDao {
+ * {@literal @}Insert(onConflict = OnConflictStrategy.REPLACE)
+ * public void insertUsers(User... users);
+ * {@literal @}Insert
+ * public void insertBoth(User user1, User user2);
+ * {@literal @}Insert
+ * public void insertWithFriends(User user, List<User> friends);
+ * }
+ * </pre>
+ *
+ * @see Update
+ * @see Delete
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Insert {
+ /**
+ * What to do if a conflict happens.
+ * @see <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a>
+ *
+ * @return How to handle conflicts. Defaults to {@link OnConflictStrategy#ABORT}.
+ */
+ @OnConflictStrategy
+ int onConflict() default OnConflictStrategy.ABORT;
+}
diff --git a/room/common/src/main/java/androidx/room/OnConflictStrategy.java b/room/common/src/main/java/androidx/room/OnConflictStrategy.java
new file mode 100644
index 0000000..e27e9e1
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/OnConflictStrategy.java
@@ -0,0 +1,56 @@
+/*
+ * 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 androidx.room;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Set of conflict handling strategies for various {@link Dao} methods.
+ * <p>
+ * Check <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a> for
+ * details.
+ */
+@Retention(SOURCE)
+@IntDef({OnConflictStrategy.REPLACE, OnConflictStrategy.ROLLBACK, OnConflictStrategy.ABORT,
+ OnConflictStrategy.FAIL, OnConflictStrategy.IGNORE})
+public @interface OnConflictStrategy {
+ /**
+ * OnConflict strategy constant to replace the old data and continue the transaction.
+ */
+ int REPLACE = 1;
+ /**
+ * OnConflict strategy constant to rollback the transaction.
+ */
+ int ROLLBACK = 2;
+ /**
+ * OnConflict strategy constant to abort the transaction.
+ */
+ int ABORT = 3;
+ /**
+ * OnConflict strategy constant to fail the transaction.
+ */
+ int FAIL = 4;
+ /**
+ * OnConflict strategy constant to ignore the conflict.
+ */
+ int IGNORE = 5;
+
+}
diff --git a/room/common/src/main/java/androidx/room/PrimaryKey.java b/room/common/src/main/java/androidx/room/PrimaryKey.java
new file mode 100644
index 0000000..7b01221
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/PrimaryKey.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a field in an {@link Entity} as the primary key.
+ * <p>
+ * If you would like to define a composite primary key, you should use {@link Entity#primaryKeys()}
+ * method.
+ * <p>
+ * Each {@link Entity} must declare a primary key unless one of its super classes declares a
+ * primary key. If both an {@link Entity} and its super class defines a {@code PrimaryKey}, the
+ * child's {@code PrimaryKey} definition will override the parent's {@code PrimaryKey}.
+ * <p>
+ * If {@code PrimaryKey} annotation is used on a {@link Embedded}d field, all columns inherited
+ * from that embedded field becomes the composite primary key (including its grand children
+ * fields).
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.CLASS)
+public @interface PrimaryKey {
+ /**
+ * Set to true to let SQLite generate the unique id.
+ * <p>
+ * When set to {@code true}, the SQLite type affinity for the field should be {@code INTEGER}.
+ * <p>
+ * If the field type is {@code long} or {@code int} (or its TypeConverter converts it to a
+ * {@code long} or {@code int}), {@link Insert} methods treat {@code 0} as not-set while
+ * inserting the item.
+ * <p>
+ * If the field's type is {@link Integer} or {@link Long} (or its TypeConverter converts it to
+ * an {@link Integer} or a {@link Long}), {@link Insert} methods treat {@code null} as
+ * not-set while inserting the item.
+ *
+ * @return Whether the primary key should be auto-generated by SQLite or not. Defaults
+ * to false.
+ */
+ boolean autoGenerate() default false;
+}
diff --git a/room/common/src/main/java/androidx/room/Query.java b/room/common/src/main/java/androidx/room/Query.java
new file mode 100644
index 0000000..34af09b
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Query.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 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 androidx.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 a {@link Dao} annotated class as a query method.
+ * <p>
+ * The value of the annotation includes the query that will be run when this method is called. This
+ * query is <b>verified at compile time</b> by Room to ensure that it compiles fine against the
+ * database.
+ * <p>
+ * The arguments of the method will be bound to the bind arguments in the SQL statement. See
+ * <href="https://www.sqlite.org/c3ref/bind_blob.html">SQLite's binding documentation</> for
+ * details of bind arguments in SQLite.
+ * <p>
+ * Room only supports named bind parameter {@code :name} to avoid any confusion between the
+ * method parameters and the query bind parameters.
+ * <p>
+ * Room will automatically bind the parameters of the method into the bind arguments. This is done
+ * by matching the name of the parameters to the name of the bind arguments.
+ * <pre>
+ * {@literal @}Query("SELECT * FROM user WHERE user_name LIKE :name AND last_name LIKE :last")
+ * public abstract List<User> findUsersByNameAndLastName(String name, String last);
+ * </pre>
+ * <p>
+ * As an extension over SQLite bind arguments, Room supports binding a list of parameters to the
+ * query. At runtime, Room will build the correct query to have matching number of bind arguments
+ * depending on the number of items in the method parameter.
+ * <pre>
+ * {@literal @}Query("SELECT * FROM user WHERE uid IN(:userIds)")
+ * public abstract List<User> findByIds(int[] userIds);
+ * </pre>
+ * For the example above, if the {@code userIds} is an array of 3 elements, Room will run the
+ * query as: {@code SELECT * FROM user WHERE uid IN(?, ?, ?)} and bind each item in the
+ * {@code userIds} array into the statement.
+ * <p>
+ * There are 3 types of queries supported in {@code Query} methods: SELECT, UPDATE and DELETE.
+ * <p>
+ * For SELECT queries, Room will infer the result contents from the method's return type and
+ * generate the code that will automatically convert the query result into the method's return
+ * type. For single result queries, the return type can be any java object. For queries that return
+ * multiple values, you can use {@link java.util.List} or {@code Array}. In addition to these, any
+ * query may return {@link android.database.Cursor Cursor} or any query result can be wrapped in
+ * a {@link androidx.lifecycle.LiveData LiveData}.
+ * <p>
+ * <b>RxJava2</b> If you are using RxJava2, you can also return {@code Flowable<T>} or
+ * {@code Publisher<T>} from query methods. Since Reactive Streams does not allow {@code null}, if
+ * the query returns a nullable type, it will not dispatch anything if the value is {@code null}
+ * (like fetching an {@link Entity} row that does not exist).
+ * You can return {@code Flowable<T[]>} or {@code Flowable<List<T>>} to workaround this limitation.
+ * <p>
+ * Both {@code Flowable<T>} and {@code Publisher<T>} will observe the database for changes and
+ * re-dispatch if data changes. If you want to query the database without observing changes, you can
+ * use {@code Maybe<T>} or {@code Single<T>}. If a {@code Single<T>} query returns {@code null},
+ * Room will throw
+ * {@link androidx.room.EmptyResultSetException EmptyResultSetException}.
+ * <p>
+ * UPDATE or DELETE queries can return {@code void} or {@code int}. If it is an {@code int},
+ * the value is the number of rows affected by this query.
+ * <p>
+ * You can return arbitrary POJOs from your query methods as long as the fields of the POJO match
+ * the column names in the query result.
+ * For example, if you have class:
+ * <pre>
+ * class UserName {
+ * public String name;
+ * {@literal @}ColumnInfo(name = "last_name")
+ * public String lastName;
+ * }
+ * </pre>
+ * You can write a query like this:
+ * <pre>
+ * {@literal @}Query("SELECT last_name, name FROM user WHERE uid = :userId LIMIT 1")
+ * public abstract UserName findOneUserName(int userId);
+ * </pre>
+ * And Room will create the correct implementation to convert the query result into a
+ * {@code UserName} object. If there is a mismatch between the query result and the fields of the
+ * POJO, as long as there is at least 1 field match, Room prints a
+ * {@link RoomWarnings#CURSOR_MISMATCH} warning and sets as many fields as it can.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface Query {
+ /**
+ * The SQLite query to be run.
+ * @return The query to be run.
+ */
+ String value();
+}
diff --git a/room/common/src/main/java/androidx/room/RawQuery.java b/room/common/src/main/java/androidx/room/RawQuery.java
new file mode 100644
index 0000000..5abcecc
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/RawQuery.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2018 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 androidx.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 a {@link Dao} annotated class as a raw query method where you can pass the
+ * query as a {@link androidx.sqlite.db.SupportSQLiteQuery SupportSQLiteQuery}.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * User getUserViaQuery(SupportSQLiteQuery query);
+ * }
+ * SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1",
+ * new Object[]{userId});
+ * User user2 = rawDao.getUserViaQuery(query);
+ * </pre>
+ * <p>
+ * Room will generate the code based on the return type of the function and failure to
+ * pass a proper query will result in a runtime failure or an undefined result.
+ * <p>
+ * If you know the query at compile time, you should always prefer {@link Query} since it validates
+ * the query at compile time and also generates more efficient code since Room can compute the
+ * query result at compile time (e.g. it does not need to account for possibly missing columns in
+ * the response).
+ * <p>
+ * On the other hand, {@code RawQuery} serves as an escape hatch where you can build your own
+ * SQL query at runtime but still use Room to convert it into objects.
+ * <p>
+ * {@code RawQuery} methods must return a non-void type. If you want to execute a raw query that
+ * does not return any value, use {@link androidx.room.RoomDatabase#query
+ * RoomDatabase#query} methods.
+ * <p>
+ * RawQuery methods can only be used for read queries. For write queries, use
+ * {@link androidx.room.RoomDatabase#getOpenHelper
+ * RoomDatabase.getOpenHelper().getWritableDatabase()}.
+ * <p>
+ * <b>Observable Queries:</b>
+ * <p>
+ * {@code RawQuery} methods can return observable types but you need to specify which tables are
+ * accessed in the query using the {@link #observedEntities()} field in the annotation.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData<List<User>> getUsers(SupportSQLiteQuery query);
+ * }
+ * LiveData<List<User>> liveUsers = rawDao.getUsers(
+ * new SimpleSQLiteQuery("SELECT * FROM User ORDER BY name DESC"));
+ * </pre>
+ * <b>Returning Pojos:</b>
+ * <p>
+ * RawQueries can also return plain old java objects, similar to {@link Query} methods.
+ * <pre>
+ * public class NameAndLastName {
+ * public final String name;
+ * public final String lastName;
+ *
+ * public NameAndLastName(String name, String lastName) {
+ * this.name = name;
+ * this.lastName = lastName;
+ * }
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * NameAndLastName getNameAndLastName(SupportSQLiteQuery query);
+ * }
+ * NameAndLastName result = rawDao.getNameAndLastName(
+ * new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ?", new Object[]{userId}))
+ * // or
+ * NameAndLastName result = rawDao.getNameAndLastName(
+ * new SimpleSQLiteQuery("SELECT name, lastName FROM User WHERE id = ?",
+ * new Object[]{userId})))
+ * </pre>
+ * <p>
+ * <b>Pojos with Embedded Fields:</b>
+ * <p>
+ * {@code RawQuery} methods can return pojos that include {@link Embedded} fields as well.
+ * <pre>
+ * public class UserAndPet {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Embedded
+ * public Pet pet;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * UserAndPet getUserAndPet(SupportSQLiteQuery query);
+ * }
+ * UserAndPet received = rawDao.getUserAndPet(
+ * new SimpleSQLiteQuery("SELECT * FROM User, Pet WHERE User.id = Pet.userId LIMIT 1"))
+ * </pre>
+ *
+ * <b>Relations:</b>
+ * <p>
+ * {@code RawQuery} return types can also be objects with {@link Relation Relations}.
+ * <pre>
+ * public class UserAndAllPets {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
+ * public List<Pet> pets;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * List<UserAndAllPets> getUsersAndAllPets(SupportSQLiteQuery query);
+ * }
+ * List<UserAndAllPets> result = rawDao.getUsersAndAllPets(
+ * new SimpleSQLiteQuery("SELECT * FROM users"));
+ * </pre>
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface RawQuery {
+ /**
+ * Denotes the list of entities which are accessed in the provided query and should be observed
+ * for invalidation if the query is observable.
+ * <p>
+ * The listed classes should either be annotated with {@link Entity} or they should reference to
+ * at least 1 Entity (via {@link Embedded} or {@link Relation}).
+ * <p>
+ * Providing this field in a non-observable query has no impact.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData<List<User>> getUsers(String query);
+ * }
+ * LiveData<List<User>> liveUsers = rawDao.getUsers("select * from User ORDER BY name
+ * DESC");
+ * </pre>
+ *
+ * @return List of entities that should invalidate the query if changed.
+ */
+ Class[] observedEntities() default {};
+}
diff --git a/room/common/src/main/java/androidx/room/Relation.java b/room/common/src/main/java/androidx/room/Relation.java
new file mode 100644
index 0000000..33dd31e
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Relation.java
@@ -0,0 +1,142 @@
+/*
+ * 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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A convenience annotation which can be used in a Pojo to automatically fetch relation entities.
+ * When the Pojo is returned from a query, all of its relations are also fetched by Room.
+ *
+ * <pre>
+ * {@literal @}Entity
+ * public class Pet {
+ * {@literal @} PrimaryKey
+ * int id;
+ * int userId;
+ * String name;
+ * // other fields
+ * }
+ * public class UserNameAndAllPets {
+ * public int id;
+ * public String name;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
+ * public List<Pet> pets;
+ * }
+ *
+ * {@literal @}Dao
+ * public interface UserPetDao {
+ * {@literal @}Query("SELECT id, name from User")
+ * public List<UserNameAndAllPets> loadUserAndPets();
+ * }
+ * </pre>
+ * <p>
+ * The type of the field annotated with {@code Relation} must be a {@link java.util.List} or
+ * {@link java.util.Set}. By default, the {@link Entity} type is inferred from the return type.
+ * If you would like to return a different object, you can specify the {@link #entity()} property
+ * in the annotation.
+ * <pre>
+ * public class User {
+ * int id;
+ * // other fields
+ * }
+ * public class PetNameAndId {
+ * int id;
+ * String name;
+ * }
+ * public class UserAllPets {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class)
+ * public List<PetNameAndId> pets;
+ * }
+ * {@literal @}Dao
+ * public interface UserPetDao {
+ * {@literal @}Query("SELECT * from User")
+ * public List<UserAllPets> loadUserAndPets();
+ * }
+ * </pre>
+ * <p>
+ * In the example above, {@code PetNameAndId} is a regular Pojo but all of fields are fetched
+ * from the {@code entity} defined in the {@code @Relation} annotation (<i>Pet</i>).
+ * {@code PetNameAndId} could also define its own relations all of which would also be fetched
+ * automatically.
+ * <p>
+ * If you would like to specify which columns are fetched from the child {@link Entity}, you can
+ * use {@link #projection()} property in the {@code Relation} annotation.
+ * <pre>
+ * public class UserAndAllPets {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class,
+ * projection = {"name"})
+ * public List<String> petNames;
+ * }
+ * </pre>
+ * <p>
+ * Note that {@code @Relation} annotation can be used only in Pojo classes, an {@link Entity} class
+ * cannot have relations. This is a design decision to avoid common pitfalls in {@link Entity}
+ * setups. You can read more about it in the main Room documentation. When loading data, you can
+ * simply work around this limitation by creating Pojo classes that extend the {@link Entity}.
+ * <p>
+ * Note that the {@code @Relation} annotated field cannot be a constructor parameter, it must be
+ * public or have a public setter.
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.CLASS)
+public @interface Relation {
+ /**
+ * The entity to fetch the item from. You don't need to set this if the entity matches the
+ * type argument in the return type.
+ *
+ * @return The entity to fetch from. By default, inherited from the return type.
+ */
+ Class entity() default Object.class;
+
+ /**
+ * Reference field in the parent Pojo.
+ * <p>
+ * If you would like to access to a sub item of a {@link Embedded}d field, you can use
+ * the {@code .} notation.
+ * <p>
+ * For instance, if you have a {@link Embedded}d field named {@code user} with a sub field
+ * {@code id}, you can reference it via {@code user.id}.
+ * <p>
+ * This value will be matched against the value defined in {@link #entityColumn()}.
+ *
+ * @return The field reference in the parent object.
+ */
+ String parentColumn();
+
+ /**
+ * The field path to match in the {@link #entity()}. This value will be matched against the
+ * value defined in {@link #parentColumn()}.
+ */
+ String entityColumn();
+
+ /**
+ * If sub fields should be fetched from the entity, you can specify them using this field.
+ * <p>
+ * By default, inferred from the the return type.
+ *
+ * @return The list of columns to be selected from the {@link #entity()}.
+ */
+ String[] projection() default {};
+}
diff --git a/room/common/src/main/java/androidx/room/RoomMasterTable.java b/room/common/src/main/java/androidx/room/RoomMasterTable.java
new file mode 100644
index 0000000..6c8c855
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/RoomMasterTable.java
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.room;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Schema information about Room's master table.
+ *
+ * @hide
+ */
+@SuppressWarnings("WeakerAccess")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RoomMasterTable {
+ /**
+ * The master table where room keeps its metadata information.
+ */
+ public static final String TABLE_NAME = "room_master_table";
+ // must match the runtime property Room#MASTER_TABLE_NAME
+ public static final String NAME = "room_master_table";
+ private static final String COLUMN_ID = "id";
+ private static final String COLUMN_IDENTITY_HASH = "identity_hash";
+ public static final String DEFAULT_ID = "42";
+
+ public static final String CREATE_QUERY = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
+ + COLUMN_ID + " INTEGER PRIMARY KEY,"
+ + COLUMN_IDENTITY_HASH + " TEXT)";
+
+ public static final String READ_QUERY = "SELECT " + COLUMN_IDENTITY_HASH
+ + " FROM " + TABLE_NAME + " WHERE "
+ + COLUMN_ID + " = " + DEFAULT_ID + " LIMIT 1";
+
+ /**
+ * We don't escape here since we know what we are passing.
+ */
+ public static String createInsertQuery(String hash) {
+ return "INSERT OR REPLACE INTO " + TABLE_NAME + " ("
+ + COLUMN_ID + "," + COLUMN_IDENTITY_HASH + ")"
+ + " VALUES(" + DEFAULT_ID + ", \"" + hash + "\")";
+ }
+}
diff --git a/room/common/src/main/java/androidx/room/RoomWarnings.java b/room/common/src/main/java/androidx/room/RoomWarnings.java
new file mode 100644
index 0000000..a4122bd
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/RoomWarnings.java
@@ -0,0 +1,136 @@
+/*
+ * 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 androidx.room;
+
+/**
+ * The list of warnings that are produced by Room.
+ * <p>
+ * You can use these values inside a {@link SuppressWarnings} annotation to disable the warnings.
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+public class RoomWarnings {
+ /**
+ * The warning dispatched by Room when the return value of a {@link Query} method does not
+ * exactly match the fields in the query result.
+ */
+ // if you change this, don't forget to change androidx.room.vo.Warning
+ public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
+
+ /**
+ * Reported when Room cannot verify database queries during compilation due to lack of
+ * tmp dir access in JVM.
+ */
+ public static final String MISSING_JAVA_TMP_DIR = "ROOM_MISSING_JAVA_TMP_DIR";
+
+ /**
+ * Reported when Room cannot verify database queries during compilation. This usually happens
+ * when it cannot find the SQLite JDBC driver on the host machine.
+ * <p>
+ * Room can function without query verification but its functionality will be limited.
+ */
+ public static final String CANNOT_CREATE_VERIFICATION_DATABASE =
+ "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
+
+ /**
+ * Reported when an {@link Entity} field that is annotated with {@link Embedded} has a
+ * sub field which is annotated with {@link PrimaryKey} but the {@link PrimaryKey} is dropped
+ * while composing it into the parent object.
+ */
+ public static final String PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED =
+ "ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED";
+
+ /**
+ * Reported when an {@link Entity} field that is annotated with {@link Embedded} has a
+ * sub field which has a {@link ColumnInfo} annotation with {@code index = true}.
+ * <p>
+ * You can re-define the index in the containing {@link Entity}.
+ */
+ public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED =
+ "ROOM_EMBEDDED_INDEX_IS_DROPPED";
+
+ /**
+ * Reported when an {@link Entity} that has a {@link Embedded}d field whose type is another
+ * {@link Entity} and that {@link Entity} has some indices defined.
+ * These indices will NOT be created in the containing {@link Entity}. If you want to preserve
+ * them, you can re-define them in the containing {@link Entity}.
+ */
+ public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED =
+ "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
+
+ /**
+ * Reported when an {@link Entity}'s parent declares an {@link Index}. Room does not
+ * automatically inherit these indices to avoid hidden costs or unexpected constraints.
+ * <p>
+ * If you want your child class to have the indices of the parent, you must re-declare
+ * them in the child class. Alternatively, you can set {@link Entity#inheritSuperIndices()}
+ * to {@code true}.
+ */
+ public static final String INDEX_FROM_PARENT_IS_DROPPED =
+ "ROOM_PARENT_INDEX_IS_DROPPED";
+
+ /**
+ * Reported when an {@link Entity} inherits a field from its super class and the field has a
+ * {@link ColumnInfo} annotation with {@code index = true}.
+ * <p>
+ * These indices are dropped for the {@link Entity} and you would need to re-declare them if
+ * you want to keep them. Alternatively, you can set {@link Entity#inheritSuperIndices()}
+ * to {@code true}.
+ */
+ public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED =
+ "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
+
+ /**
+ * Reported when a {@link Relation} {@link Entity}'s SQLite column type does not match the type
+ * in the parent. Room will still do the matching using {@code String} representations.
+ */
+ public static final String RELATION_TYPE_MISMATCH = "ROOM_RELATION_TYPE_MISMATCH";
+
+ /**
+ * Reported when a `room.schemaLocation` argument is not provided into the annotation processor.
+ * You can either set {@link Database#exportSchema()} to {@code false} or provide
+ * `room.schemaLocation` to the annotation processor. You are strongly adviced to provide it
+ * and also commit them into your version control system.
+ */
+ public static final String MISSING_SCHEMA_LOCATION = "ROOM_MISSING_SCHEMA_LOCATION";
+
+ /**
+ * When there is a foreign key from Entity A to Entity B, it is a good idea to index the
+ * reference columns in B, otherwise, each modification on Entity A will trigger a full table
+ * scan on Entity B.
+ * <p>
+ * If Room cannot find a proper index in the child entity (Entity B in this case), Room will
+ * print this warning.
+ */
+ 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";
+
+ /**
+ * Reported when a @Query method returns a Pojo that has relations but the method is not
+ * annotated with @Transaction. Relations are run as separate queries and if the query is not
+ * run inside a transaction, it might return inconsistent results from the database.
+ */
+ public static final String RELATION_QUERY_WITHOUT_TRANSACTION =
+ "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
+}
diff --git a/room/common/src/main/java/androidx/room/SkipQueryVerification.java b/room/common/src/main/java/androidx/room/SkipQueryVerification.java
new file mode 100644
index 0000000..9d49746
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/SkipQueryVerification.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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Skips database verification for the annotated element.
+ * <p>
+ * If it is a class annotated with {@link Database}, none of the queries for the database will
+ * be verified at compile time.
+ * <p>
+ * If it is a class annotated with {@link Dao}, none of the queries in the Dao class will
+ * be verified at compile time.
+ * <p>
+ * If it is a method in a Dao class, just the method's sql verification will be skipped.
+ * <p>
+ * You should use this as the last resort if Room cannot properly understand your query and you are
+ * 100% sure it works. Removing validation may limit the functionality of Room since it won't be
+ * able to understand the query response.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface SkipQueryVerification {
+}
diff --git a/room/common/src/main/java/androidx/room/Transaction.java b/room/common/src/main/java/androidx/room/Transaction.java
new file mode 100644
index 0000000..0941673
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Transaction.java
@@ -0,0 +1,84 @@
+/*
+ * 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 androidx.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 a {@link Dao} class as a transaction method.
+ * <p>
+ * When used on a non-abstract method of an abstract {@link Dao} class,
+ * 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>
+ * <p>
+ * When used on a {@link Query} method that has a {@code Select} statement, the generated code for
+ * the Query will be run in a transaction. There are 2 main cases where you may want to do that:
+ * <ol>
+ * <li>If the result of the query is fairly big, it is better to run it inside a transaction
+ * to receive a consistent result. Otherwise, if the query result does not fit into a single
+ * {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to
+ * changes in the database in between cursor window swaps.
+ * <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are
+ * queried separately. To receive consistent results between these queries, you probably want
+ * to run them in a single transaction.
+ * </ol>
+ * Example:
+ * <pre>
+ * class ProductWithReviews extends Product {
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class)
+ * public List<Review> reviews;
+ * }
+ * {@literal @}Dao
+ * public interface ProductDao {
+ * {@literal @}Transaction {@literal @}Query("SELECT * from products")
+ * public List<ProductWithReviews> loadAll();
+ * }
+ * </pre>
+ * If the query is an async query (e.g. returns a {@link androidx.lifecycle.LiveData LiveData}
+ * or RxJava Flowable, the transaction is properly handled when the query is run, not when the
+ * method is called.
+ * <p>
+ * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no
+ * impact because they are always run inside a transaction. Similarly, if it is annotated with
+ * {@link Query} but runs an update or delete statement, it is automatically wrapped in a
+ * transaction.
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Transaction {
+}
diff --git a/room/common/src/main/java/androidx/room/TypeConverter.java b/room/common/src/main/java/androidx/room/TypeConverter.java
new file mode 100644
index 0000000..b95ef09
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/TypeConverter.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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method as a type converter. A class can have as many @TypeConverter methods as it needs.
+ * <p>
+ * Each converter method should receive 1 parameter and have non-void return type.
+ *
+ * <pre>
+ * // example converter for java.util.Date
+ * public static class Converters {
+ * {@literal @}TypeConverter
+ * public Date fromTimestamp(Long value) {
+ * return value == null ? null : new Date(value);
+ * }
+ *
+ * {@literal @}TypeConverter
+ * public Long dateToTimestamp(Date date) {
+ * if (date == null) {
+ * return null;
+ * } else {
+ * return date.getTime();
+ * }
+ * }
+ *}
+ * </pre>
+ * @see TypeConverters
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface TypeConverter {
+}
diff --git a/room/common/src/main/java/androidx/room/TypeConverters.java b/room/common/src/main/java/androidx/room/TypeConverters.java
new file mode 100644
index 0000000..e68525b
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/TypeConverters.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 androidx.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Specifies additional type converters that Room can use. The TypeConverter is added to the scope
+ * of the element so if you put it on a class / interface, all methods / fields in that class will
+ * be able to use the converters.
+ * <ul>
+ * <li>If you put it on a {@link Database}, all Daos and Entities in that database will be able to
+ * use it.
+ * <li>If you put it on a {@link Dao}, all methods in the Dao will be able to use it.
+ * <li>If you put it on an {@link Entity}, all fields of the Entity will be able to use it.
+ * <li>If you put it on a POJO, all fields of the POJO will be able to use it.
+ * <li>If you put it on an {@link Entity} field, only that field will be able to use it.
+ * <li>If you put it on a {@link Dao} method, all parameters of the method will be able to use it.
+ * <li>If you put it on a {@link Dao} method parameter, just that field will be able to use it.
+ * </ul>
+ * @see TypeConverter
+ */
+@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface TypeConverters {
+ /**
+ * The list of type converter classes. If converter methods are not static, Room will create
+ * an instance of these classes.
+ *
+ * @return The list of classes that contains the converter methods.
+ */
+ Class<?>[] value();
+}
diff --git a/room/common/src/main/java/androidx/room/Update.java b/room/common/src/main/java/androidx/room/Update.java
new file mode 100644
index 0000000..7482374
--- /dev/null
+++ b/room/common/src/main/java/androidx/room/Update.java
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.room;
+
+/**
+ * Marks a method in a {@link Dao} annotated class as an update method.
+ * <p>
+ * The implementation of the method will update its parameters in the database if they already
+ * exists (checked by primary keys). If they don't already exists, this option will not change the
+ * database.
+ * <p>
+ * All of the parameters of the Update method must either be classes annotated with {@link Entity}
+ * or collections/array of it.
+ *
+ * @see Insert
+ * @see Delete
+ */
+public @interface Update {
+ /**
+ * What to do if a conflict happens.
+ * @see <a href="https://sqlite.org/lang_conflict.html">SQLite conflict documentation</a>
+ *
+ * @return How to handle conflicts. Defaults to {@link OnConflictStrategy#ABORT}.
+ */
+ @OnConflictStrategy
+ int onConflict() default OnConflictStrategy.ABORT;
+}
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index 3599893..7a4370a 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -44,8 +44,8 @@
// taken from ButterKnife
def logger = new com.android.build.gradle.internal.LoggerWrapper(project.logger)
def sdkHandler = new com.android.build.gradle.internal.SdkHandler(project, logger)
- compile(project(":room:common"))
- compile(project(":room:migration"))
+ compile(project(":room:room-common"))
+ compile(project(":room:room-migration"))
compile(KOTLIN_STDLIB)
compile(AUTO_COMMON)
compile(JAVAPOET)
@@ -54,16 +54,16 @@
compile(KOTLIN_METADATA)
compile(APACHE_COMMONS_CODEC)
testCompile(GOOGLE_COMPILE_TESTING)
- testCompile project(":paging:common")
+ testCompile project(":paging:paging-common")
testCompile(JUNIT)
testCompile(INTELLIJ_ANNOTATIONS)
testCompile(JSR250)
testCompile(MOCKITO_CORE)
testCompile fileTree(dir: "${sdkHandler.sdkFolder}/platforms/android-$SupportConfig.CURRENT_SDK_VERSION/",
include : "android.jar")
- testCompile fileTree(dir: "${new File(project(":room:runtime").buildDir, "libJar")}",
+ testCompile fileTree(dir: "${new File(project(":room:room-runtime").buildDir, "libJar")}",
include : "*.jar")
- testCompile fileTree(dir: "${new File(project(":persistence:db").buildDir, "libJar")}",
+ testCompile fileTree(dir: "${new File(project(":sqlite:sqlite").buildDir, "libJar")}",
include : "*.jar")
testCompile files(org.gradle.internal.jvm.Jvm.current().getToolsJar())
}
@@ -74,13 +74,13 @@
inputs.file("$projectDir/SQLite.g4")
classpath configurations.runtime
main "org.antlr.v4.Tool"
- args "SQLite.g4", "-visitor", "-o", new File(outFolder, "android/arch/persistence/room/parser").path,
- "-package", "android.arch.persistence.room.parser"
+ args "SQLite.g4", "-visitor", "-o", new File(outFolder, "androidx/room/parser").path,
+ "-package", "androidx.room.parser"
}
tasks.findByName("compileKotlin").dependsOn(generateAntlrTask)
-tasks.findByName("compileKotlin").dependsOn(":room:runtime:jarDebug")
-tasks.findByName("compileKotlin").dependsOn(":persistence:db:jarDebug")
+tasks.findByName("compileKotlin").dependsOn(":room:room-runtime:jarDebug")
+tasks.findByName("compileKotlin").dependsOn(":sqlite:sqlite:jarDebug")
supportLibrary {
name = "Android Room Compiler"
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/RoomProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/RoomProcessor.kt
deleted file mode 100644
index 57070fd..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/RoomProcessor.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.DatabaseProcessor
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.vo.DaoMethod
-import android.arch.persistence.room.vo.Warning
-import android.arch.persistence.room.writer.DaoWriter
-import android.arch.persistence.room.writer.DatabaseWriter
-import com.google.auto.common.BasicAnnotationProcessor
-import com.google.auto.common.MoreElements
-import com.google.common.collect.SetMultimap
-import java.io.File
-import javax.lang.model.SourceVersion
-import javax.lang.model.element.Element
-
-/**
- * The annotation processor for Room.
- */
-class RoomProcessor : BasicAnnotationProcessor() {
- override fun initSteps(): MutableIterable<ProcessingStep>? {
- val context = Context(processingEnv)
- return arrayListOf(DatabaseProcessingStep(context))
- }
-
- override fun getSupportedOptions(): MutableSet<String> {
- return Context.ARG_OPTIONS.toMutableSet()
- }
-
- override fun getSupportedSourceVersion(): SourceVersion {
- return SourceVersion.latest()
- }
-
- class DatabaseProcessingStep(context: Context) : ContextBoundProcessingStep(context) {
- override fun process(
- elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>
- ): MutableSet<Element> {
- // TODO multi step support
- val databases = elementsByAnnotation[Database::class.java]
- ?.map {
- DatabaseProcessor(context, MoreElements.asType(it)).process()
- }
- val allDaoMethods = databases?.flatMap { it.daoMethods }
- allDaoMethods?.let {
- prepareDaosForWriting(databases, it)
- it.forEach {
- DaoWriter(it.dao, context.processingEnv).write(context.processingEnv)
- }
- }
-
- databases?.forEach { db ->
- DatabaseWriter(db).write(context.processingEnv)
- if (db.exportSchema) {
- val schemaOutFolder = context.schemaOutFolder
- if (schemaOutFolder == null) {
- context.logger.w(Warning.MISSING_SCHEMA_LOCATION, db.element,
- ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY)
- } else {
- if (!schemaOutFolder.exists()) {
- schemaOutFolder.mkdirs()
- }
- val qName = db.element.qualifiedName.toString()
- val dbSchemaFolder = File(schemaOutFolder, qName)
- if (!dbSchemaFolder.exists()) {
- dbSchemaFolder.mkdirs()
- }
- db.exportSchema(File(dbSchemaFolder, "${db.version}.json"))
- }
- }
- }
- return mutableSetOf()
- }
- override fun annotations(): MutableSet<out Class<out Annotation>> {
- return mutableSetOf(Database::class.java, Dao::class.java, Entity::class.java)
- }
-
- /**
- * Traverses all dao methods and assigns them suffix if they are used in multiple databases.
- */
- private fun prepareDaosForWriting(
- databases: List<android.arch.persistence.room.vo.Database>,
- daoMethods: List<DaoMethod>) {
- daoMethods.groupBy { it.dao.typeName }
- // if used only in 1 database, nothing to do.
- .filter { entry -> entry.value.size > 1 }
- .forEach { entry ->
- entry.value.groupBy { daoMethod ->
- // first suffix guess: Database's simple name
- val db = databases.first { db -> db.daoMethods.contains(daoMethod) }
- db.typeName.simpleName()
- }.forEach { (dbName, methods) ->
- if (methods.size == 1) {
- //good, db names do not clash, use db name as suffix
- methods.first().dao.setSuffix(dbName)
- } else {
- // ok looks like a dao is used in 2 different databases both of
- // which have the same name. enumerate.
- methods.forEachIndexed { index, method ->
- method.dao.setSuffix("${dbName}_$index")
- }
- }
- }
- }
- }
- }
-
- abstract class ContextBoundProcessingStep(val context: Context) : ProcessingStep
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/KotlinMetadataProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/KotlinMetadataProcessor.kt
deleted file mode 100644
index 8ff1aa8..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/KotlinMetadataProcessor.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2018 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.ext
-
-import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
-import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
-import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
-import org.jetbrains.kotlin.serialization.ProtoBuf
-import javax.lang.model.element.ExecutableElement
-
-/**
- * Utility interface for processors that wants to run kotlin specific code.
- */
-interface KotlinMetadataProcessor : KotlinMetadataUtils {
- /**
- * Returns the parameter names of the function if all have names embedded in the metadata.
- */
- fun KotlinClassMetadata.getParameterNames(method: ExecutableElement): List<String>? {
- val valueParameterList = this.data.getFunctionOrNull(method)?.valueParameterList
- ?: findConstructor(method)?.valueParameterList
- ?: return null
- return if (valueParameterList.all { it.hasName() }) {
- valueParameterList.map {
- data.nameResolver.getName(it.name)
- .asString()
- .replace("`", "")
- .removeSuffix("?")
- .trim()
- }
- } else {
- null
- }
- }
-
- /**
- * Finds the kotlin metadata for a constructor.
- */
- private fun KotlinClassMetadata.findConstructor(
- executableElement: ExecutableElement
- ): ProtoBuf.Constructor? {
- val (nameResolver, classProto) = data
- val jvmSignature = executableElement.jvmMethodSignature
- // find constructor
- return classProto.constructorList.singleOrNull {
- it.getJvmConstructorSignature(nameResolver, classProto.typeTable) == jvmSignature
- }
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
deleted file mode 100644
index fbd54c4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * Copyright (C) 2016 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.
- */
-
-@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-
-package android.arch.persistence.room.ext
-
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import org.jetbrains.kotlin.load.java.JvmAbi
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.AnnotationValue
-import javax.lang.model.element.Element
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.lang.model.element.VariableElement
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-import javax.lang.model.type.WildcardType
-import javax.lang.model.util.SimpleAnnotationValueVisitor6
-import javax.lang.model.util.SimpleTypeVisitor7
-import javax.lang.model.util.Types
-import kotlin.reflect.KClass
-
-fun Element.hasAnyOf(vararg modifiers: Modifier): Boolean {
- return this.modifiers.any { modifiers.contains(it) }
-}
-
-fun Element.hasAnnotation(klass: KClass<out Annotation>): Boolean {
- return MoreElements.isAnnotationPresent(this, klass.java)
-}
-
-fun Element.isNonNull() =
- asType().kind.isPrimitive
- || hasAnnotation(android.support.annotation.NonNull::class)
- || hasAnnotation(org.jetbrains.annotations.NotNull::class)
-
-/**
- * gets all members including super privates. does not handle duplicate field names!!!
- */
-// TODO handle conflicts with super: b/35568142
-fun TypeElement.getAllFieldsIncludingPrivateSupers(processingEnvironment: ProcessingEnvironment):
- Set<VariableElement> {
- val myMembers = processingEnvironment.elementUtils.getAllMembers(this)
- .filter { it.kind == ElementKind.FIELD }
- .filter { it is VariableElement }
- .map { it as VariableElement }
- .toSet()
- if (superclass.kind != TypeKind.NONE) {
- return myMembers + MoreTypes.asTypeElement(superclass)
- .getAllFieldsIncludingPrivateSupers(processingEnvironment)
- } else {
- return myMembers
- }
-}
-
-// code below taken from dagger2
-// compiler/src/main/java/dagger/internal/codegen/ConfigurationAnnotations.java
-private val TO_LIST_OF_TYPES = object
- : SimpleAnnotationValueVisitor6<List<TypeMirror>, Void?>() {
- override fun visitArray(values: MutableList<out AnnotationValue>?, p: Void?): List<TypeMirror> {
- return values?.map {
- val tmp = TO_TYPE.visit(it)
- tmp
- }?.filterNotNull() ?: emptyList()
- }
-
- override fun defaultAction(o: Any?, p: Void?): List<TypeMirror>? {
- return emptyList()
- }
-}
-
-private val TO_TYPE = object : SimpleAnnotationValueVisitor6<TypeMirror, Void>() {
-
- override fun visitType(t: TypeMirror, p: Void?): TypeMirror {
- return t
- }
-
- override fun defaultAction(o: Any?, p: Void?): TypeMirror {
- throw TypeNotPresentException(o!!.toString(), null)
- }
-}
-
-fun AnnotationValue.toListOfClassTypes(): List<TypeMirror> {
- return TO_LIST_OF_TYPES.visit(this)
-}
-
-fun AnnotationValue.toType(): TypeMirror {
- return TO_TYPE.visit(this)
-}
-
-fun AnnotationValue.toClassType(): TypeMirror? {
- return TO_TYPE.visit(this)
-}
-
-fun TypeMirror.isCollection(): Boolean {
- return MoreTypes.isType(this)
- && (MoreTypes.isTypeOf(java.util.List::class.java, this)
- || MoreTypes.isTypeOf(java.util.Set::class.java, this))
-}
-
-fun Element.getAnnotationValue(annotation: Class<out Annotation>, fieldName: String): Any? {
- return MoreElements.getAnnotationMirror(this, annotation)
- .orNull()?.let {
- AnnotationMirrors.getAnnotationValue(it, fieldName)?.value
- }
-}
-
-private val ANNOTATION_VALUE_TO_INT_VISITOR = object : SimpleAnnotationValueVisitor6<Int?, Void>() {
- override fun visitInt(i: Int, p: Void?): Int? {
- return i
- }
-}
-
-private val ANNOTATION_VALUE_TO_BOOLEAN_VISITOR = object
- : SimpleAnnotationValueVisitor6<Boolean?, Void>() {
- override fun visitBoolean(b: Boolean, p: Void?): Boolean? {
- return b
- }
-}
-
-private val ANNOTATION_VALUE_TO_STRING_VISITOR = object
- : SimpleAnnotationValueVisitor6<String?, Void>() {
- override fun visitString(s: String?, p: Void?): String? {
- return s
- }
-}
-
-private val ANNOTATION_VALUE_STRING_ARR_VISITOR = object
- : SimpleAnnotationValueVisitor6<List<String>, Void>() {
- override fun visitArray(vals: MutableList<out AnnotationValue>?, p: Void?): List<String> {
- return vals?.map {
- ANNOTATION_VALUE_TO_STRING_VISITOR.visit(it)
- }?.filterNotNull() ?: emptyList()
- }
-}
-
-fun AnnotationValue.getAsInt(def: Int? = null): Int? {
- return ANNOTATION_VALUE_TO_INT_VISITOR.visit(this) ?: def
-}
-
-fun AnnotationValue.getAsString(def: String? = null): String? {
- return ANNOTATION_VALUE_TO_STRING_VISITOR.visit(this) ?: def
-}
-
-fun AnnotationValue.getAsBoolean(def: Boolean): Boolean {
- return ANNOTATION_VALUE_TO_BOOLEAN_VISITOR.visit(this) ?: def
-}
-
-fun AnnotationValue.getAsStringList(): List<String> {
- return ANNOTATION_VALUE_STRING_ARR_VISITOR.visit(this)
-}
-
-// a variant of Types.isAssignable that ignores variance.
-fun Types.isAssignableWithoutVariance(from: TypeMirror, to: TypeMirror): Boolean {
- val assignable = isAssignable(from, to)
- if (assignable) {
- return true
- }
- if (from.kind != TypeKind.DECLARED || to.kind != TypeKind.DECLARED) {
- return false
- }
- val declaredFrom = MoreTypes.asDeclared(from)
- val declaredTo = MoreTypes.asDeclared(to)
- val fromTypeArgs = declaredFrom.typeArguments
- val toTypeArgs = declaredTo.typeArguments
- // no type arguments, we don't need extra checks
- if (fromTypeArgs.isEmpty() || fromTypeArgs.size != toTypeArgs.size) {
- return false
- }
- // check erasure version first, if it does not match, no reason to proceed
- if (!isAssignable(erasure(from), erasure(to))) {
- return false
- }
- // convert from args to their upper bounds if it exists
- val fromExtendsBounds = fromTypeArgs.map {
- it.extendsBound()
- }
- // if there are no upper bound conversions, return.
- if (fromExtendsBounds.all { it == null }) {
- return false
- }
- // try to move the types of the from to their upper bounds. It does not matter for the "to"
- // because Types.isAssignable handles it as it is valid java
- return (0 until fromTypeArgs.size).all { index ->
- isAssignableWithoutVariance(
- from = fromExtendsBounds[index] ?: fromTypeArgs[index],
- to = toTypeArgs[index])
- }
-}
-
-// converts ? in Set< ? extends Foo> to Foo
-fun TypeMirror.extendsBound(): TypeMirror? {
- return this.accept(object : SimpleTypeVisitor7<TypeMirror?, Void?>() {
- override fun visitWildcard(type: WildcardType, ignored: Void?): TypeMirror? {
- return type.extendsBound
- }
- }, null)
-}
-
-/**
- * Finds the default implementation method corresponding to this Kotlin interface method.
- */
-fun Element.findKotlinDefaultImpl(typeUtils: Types): Element? {
- fun paramsMatch(ourParams: List<VariableElement>, theirParams: List<VariableElement>): Boolean {
- if (ourParams.size != theirParams.size - 1) {
- return false
- }
- ourParams.forEachIndexed { i, variableElement ->
- // Plus 1 to their index because their first param is a self object.
- if (!typeUtils.isSameType(theirParams[i + 1].asType(), variableElement.asType())) {
- return false
- }
- }
- return true
- }
-
- val parent = this.enclosingElement as TypeElement
- val innerClass = parent.enclosedElements.find {
- it.kind == ElementKind.CLASS && it.simpleName.contentEquals(JvmAbi.DEFAULT_IMPLS_CLASS_NAME)
- } ?: return null
- return innerClass.enclosedElements.find {
- it.kind == ElementKind.METHOD && it.simpleName == this.simpleName
- && paramsMatch(MoreElements.asExecutable(this).parameters,
- MoreElements.asExecutable(it).parameters)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
deleted file mode 100644
index fc6e693..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/javapoet_ext.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2016 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.ext
-
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeName
-import javax.lang.model.type.TypeMirror
-import kotlin.reflect.KClass
-
-val L = "\$L"
-val T = "\$T"
-val N = "\$N"
-val S = "\$S"
-
-fun KClass<*>.typeName() = ClassName.get(this.java)
-fun KClass<*>.arrayTypeName() = ArrayTypeName.of(typeName())
-fun TypeMirror.typeName() = TypeName.get(this)
-
-object SupportDbTypeNames {
- val DB: ClassName = ClassName.get("android.arch.persistence.db", "SupportSQLiteDatabase")
- val SQLITE_STMT: ClassName =
- ClassName.get("android.arch.persistence.db", "SupportSQLiteStatement")
- val SQLITE_OPEN_HELPER: ClassName =
- ClassName.get("android.arch.persistence.db", "SupportSQLiteOpenHelper")
- val SQLITE_OPEN_HELPER_CALLBACK: ClassName =
- ClassName.get("android.arch.persistence.db", "SupportSQLiteOpenHelper.Callback")
- val SQLITE_OPEN_HELPER_FACTORY: ClassName =
- ClassName.get("android.arch.persistence.db", "SupportSQLiteOpenHelper.Factory")
- val SQLITE_OPEN_HELPER_CONFIG: ClassName =
- ClassName.get("android.arch.persistence.db", "SupportSQLiteOpenHelper.Configuration")
- val SQLITE_OPEN_HELPER_CONFIG_BUILDER: ClassName =
- ClassName.get("android.arch.persistence.db",
- "SupportSQLiteOpenHelper.Configuration.Builder")
- val QUERY: ClassName =
- ClassName.get("android.arch.persistence.db", "SupportSQLiteQuery")
-}
-
-object RoomTypeNames {
- val STRING_UTIL: ClassName = ClassName.get("android.arch.persistence.room.util", "StringUtil")
- val CURSOR_CONVERTER: ClassName =
- ClassName.get("android.arch.persistence.room", "CursorConverter")
- val ROOM: ClassName = ClassName.get("android.arch.persistence.room", "Room")
- val ROOM_DB: ClassName = ClassName.get("android.arch.persistence.room", "RoomDatabase")
- val ROOM_DB_CONFIG: ClassName = ClassName.get("android.arch.persistence.room",
- "DatabaseConfiguration")
- val INSERTION_ADAPTER: ClassName =
- ClassName.get("android.arch.persistence.room", "EntityInsertionAdapter")
- val DELETE_OR_UPDATE_ADAPTER: ClassName =
- ClassName.get("android.arch.persistence.room", "EntityDeletionOrUpdateAdapter")
- val SHARED_SQLITE_STMT: ClassName =
- ClassName.get("android.arch.persistence.room", "SharedSQLiteStatement")
- val INVALIDATION_TRACKER: ClassName =
- ClassName.get("android.arch.persistence.room", "InvalidationTracker")
- val INVALIDATION_OBSERVER: ClassName =
- ClassName.get("android.arch.persistence.room.InvalidationTracker", "Observer")
- val ROOM_SQL_QUERY: ClassName =
- ClassName.get("android.arch.persistence.room", "RoomSQLiteQuery")
- val OPEN_HELPER: ClassName =
- ClassName.get("android.arch.persistence.room", "RoomOpenHelper")
- val OPEN_HELPER_DELEGATE: ClassName =
- ClassName.get("android.arch.persistence.room", "RoomOpenHelper.Delegate")
- val TABLE_INFO: ClassName =
- ClassName.get("android.arch.persistence.room.util", "TableInfo")
- val TABLE_INFO_COLUMN: ClassName =
- ClassName.get("android.arch.persistence.room.util", "TableInfo.Column")
- val TABLE_INFO_FOREIGN_KEY: ClassName =
- ClassName.get("android.arch.persistence.room.util", "TableInfo.ForeignKey")
- val TABLE_INFO_INDEX: ClassName =
- ClassName.get("android.arch.persistence.room.util", "TableInfo.Index")
- val LIMIT_OFFSET_DATA_SOURCE: ClassName =
- ClassName.get("android.arch.persistence.room.paging", "LimitOffsetDataSource")
-}
-
-object ArchTypeNames {
- val APP_EXECUTOR: ClassName =
- ClassName.get("android.arch.core.executor", "ArchTaskExecutor")
-}
-
-object PagingTypeNames {
- val DATA_SOURCE: ClassName =
- ClassName.get("android.arch.paging", "DataSource")
- val POSITIONAL_DATA_SOURCE: ClassName =
- ClassName.get("android.arch.paging", "PositionalDataSource")
- val DATA_SOURCE_FACTORY: ClassName =
- ClassName.get("android.arch.paging", "DataSource.Factory")
-}
-
-object LifecyclesTypeNames {
- val LIVE_DATA: ClassName = ClassName.get("android.arch.lifecycle", "LiveData")
- val COMPUTABLE_LIVE_DATA: ClassName = ClassName.get("android.arch.lifecycle",
- "ComputableLiveData")
-}
-
-object AndroidTypeNames {
- val CURSOR: ClassName = ClassName.get("android.database", "Cursor")
- val ARRAY_MAP: ClassName = ClassName.get("android.support.v4.util", "ArrayMap")
- val BUILD: ClassName = ClassName.get("android.os", "Build")
-}
-
-object CommonTypeNames {
- val LIST = ClassName.get("java.util", "List")
- val SET = ClassName.get("java.util", "Set")
- val STRING = ClassName.get("java.lang", "String")
- val INTEGER = ClassName.get("java.lang", "Integer")
- val OPTIONAL = ClassName.get("java.util", "Optional")
-}
-
-object GuavaBaseTypeNames {
- val OPTIONAL = ClassName.get("com.google.common.base", "Optional")
-}
-
-object GuavaUtilConcurrentTypeNames {
- val LISTENABLE_FUTURE = ClassName.get("com.google.common.util.concurrent", "ListenableFuture")
-}
-
-object RxJava2TypeNames {
- val FLOWABLE = ClassName.get("io.reactivex", "Flowable")
- val MAYBE = ClassName.get("io.reactivex", "Maybe")
- val SINGLE = ClassName.get("io.reactivex", "Single")
-}
-
-object ReactiveStreamsTypeNames {
- val PUBLISHER = ClassName.get("org.reactivestreams", "Publisher")
-}
-
-object RoomGuavaTypeNames {
- val GUAVA_ROOM = ClassName.get("android.arch.persistence.room.guava", "GuavaRoom")
-}
-
-object RoomRxJava2TypeNames {
- val RX_ROOM = ClassName.get("android.arch.persistence.room", "RxRoom")
- val RX_EMPTY_RESULT_SET_EXCEPTION = ClassName.get("android.arch.persistence.room",
- "EmptyResultSetException")
-}
-
-fun TypeName.defaultValue(): String {
- return if (!isPrimitive) {
- "null"
- } else if (this == TypeName.BOOLEAN) {
- "false"
- } else {
- "0"
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/log/RLog.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/log/RLog.kt
deleted file mode 100644
index 9832996..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/log/RLog.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2016 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.
- */
-
-@file:Suppress("unused")
-
-package android.arch.persistence.room.log
-
-import android.arch.persistence.room.vo.Warning
-import java.util.UnknownFormatConversionException
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.Element
-import javax.tools.Diagnostic
-import javax.tools.Diagnostic.Kind.ERROR
-import javax.tools.Diagnostic.Kind.NOTE
-import javax.tools.Diagnostic.Kind.WARNING
-
-class RLog(val messager: Messager, val suppressedWarnings: Set<Warning>,
- val defaultElement: Element?) {
- private fun String.safeFormat(vararg args: Any): String {
- try {
- return format(args)
- } catch (ex: UnknownFormatConversionException) {
- // the input string might be from random source in which case we rather print the
- // msg as is instead of crashing while reporting an error.
- return this
- }
- }
-
- fun d(element: Element, msg: String, vararg args: Any) {
- messager.printMessage(NOTE, msg.safeFormat(args), element)
- }
-
- fun d(msg: String, vararg args: Any) {
- messager.printMessage(NOTE, msg.safeFormat(args))
- }
-
- fun e(element: Element, msg: String, vararg args: Any) {
- messager.printMessage(ERROR, msg.safeFormat(args), element)
- }
-
- fun e(msg: String, vararg args: Any) {
- messager.printMessage(ERROR, msg.safeFormat(args), defaultElement)
- }
-
- fun w(warning: Warning, element: Element? = null, msg: String, vararg args: Any) {
- if (suppressedWarnings.contains(warning)) {
- return
- }
- messager.printMessage(WARNING, msg.safeFormat(args),
- element ?: defaultElement)
- }
-
- fun w(warning: Warning, msg: String, vararg args: Any) {
- if (suppressedWarnings.contains(warning)) {
- return
- }
- messager.printMessage(WARNING, msg.safeFormat(args), defaultElement)
- }
-
- interface Messager {
- fun printMessage(kind: Diagnostic.Kind, msg: String, element: Element? = null)
- }
-
- class ProcessingEnvMessager(val processingEnv: ProcessingEnvironment) : Messager {
- override fun printMessage(kind: Diagnostic.Kind, msg: String, element: Element?) {
- processingEnv.messager.printMessage(kind, msg, element)
- }
- }
-
- class CollectingMessager : Messager {
- private val messages = mutableMapOf<Diagnostic.Kind, MutableList<Pair<String, Element?>>> ()
- override fun printMessage(kind: Diagnostic.Kind, msg: String, element: Element?) {
- messages.getOrPut(kind, {
- arrayListOf<Pair<String, Element?>>()
- }).add(Pair(msg, element))
- }
-
- fun hasErrors() = messages.containsKey(Diagnostic.Kind.ERROR)
-
- fun writeTo(env: ProcessingEnvironment) {
- messages.forEach { pair ->
- val kind = pair.key
- pair.value.forEach {
- env.messager.printMessage(kind, it.first, it.second)
- }
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt
deleted file mode 100644
index 55a9bf9..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParsedQuery.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2016 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.parser
-
-import android.arch.persistence.room.parser.SectionType.BIND_VAR
-import android.arch.persistence.room.parser.SectionType.NEWLINE
-import android.arch.persistence.room.parser.SectionType.TEXT
-import android.arch.persistence.room.verifier.QueryResultInfo
-import org.antlr.v4.runtime.tree.TerminalNode
-
-enum class SectionType {
- BIND_VAR,
- TEXT,
- NEWLINE
-}
-
-data class Section(val text: String, val type: SectionType) {
- companion object {
- fun text(text: String) = Section(text, SectionType.TEXT)
- fun newline() = Section("", SectionType.NEWLINE)
- fun bindVar(text: String) = Section(text, SectionType.BIND_VAR)
- }
-}
-
-data class Table(val name: String, val alias: String)
-
-data class ParsedQuery(
- val original: String,
- val type: QueryType,
- val inputs: List<TerminalNode>,
- // pairs of table name and alias,
- val tables: Set<Table>,
- val syntaxErrors: List<String>,
- val runtimeQueryPlaceholder: Boolean) {
- companion object {
- val STARTS_WITH_NUMBER = "^\\?[0-9]".toRegex()
- val MISSING = ParsedQuery("missing query", QueryType.UNKNOWN, emptyList(), emptySet(),
- emptyList(), false)
- }
-
- /**
- * Optional data that might be assigned when the query is parsed inside an annotation processor.
- * User may turn this off or it might be disabled for any reason so generated code should
- * always handle not having it.
- */
- var resultInfo: QueryResultInfo? = null
-
- val sections by lazy {
- val lines = original.lines()
- val inputsByLine = inputs.groupBy { it.symbol.line }
- val sections = arrayListOf<Section>()
- lines.forEachIndexed { index, line ->
- var charInLine = 0
- inputsByLine[index + 1]?.forEach { bindVar ->
- if (charInLine < bindVar.symbol.charPositionInLine) {
- sections.add(Section.text(line.substring(charInLine,
- bindVar.symbol.charPositionInLine)))
- }
- sections.add(Section.bindVar(bindVar.text))
- charInLine = bindVar.symbol.charPositionInLine + bindVar.symbol.text.length
- }
- if (charInLine < line.length) {
- sections.add(Section.text(line.substring(charInLine)))
- }
- if (index + 1 < lines.size) {
- sections.add(Section.newline())
- }
- }
- sections
- }
-
- val bindSections by lazy { sections.filter { it.type == BIND_VAR } }
-
- private fun unnamedVariableErrors(): List<String> {
- val anonymousBindError = if (inputs.any { it.text == "?" }) {
- arrayListOf(ParserErrors.ANONYMOUS_BIND_ARGUMENT)
- } else {
- emptyList<String>()
- }
- return anonymousBindError + inputs.filter {
- it.text.matches(STARTS_WITH_NUMBER)
- }.map {
- ParserErrors.cannotUseVariableIndices(it.text, it.symbol.charPositionInLine)
- }
- }
-
- private fun unknownQueryTypeErrors(): List<String> {
- return if (QueryType.SUPPORTED.contains(type)) {
- emptyList()
- } else {
- listOf(ParserErrors.invalidQueryType(type))
- }
- }
-
- val errors by lazy {
- if (syntaxErrors.isNotEmpty()) {
- // if there is a syntax error, don't report others since they might be misleading.
- syntaxErrors
- } else {
- unnamedVariableErrors() + unknownQueryTypeErrors()
- }
- }
-
- val queryWithReplacedBindParams by lazy {
- sections.joinToString("") {
- when (it.type) {
- TEXT -> it.text
- BIND_VAR -> "?"
- NEWLINE -> "\n"
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParserErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParserErrors.kt
deleted file mode 100644
index e3b4f40..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/ParserErrors.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 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.parser
-
-object ParserErrors {
- val ANONYMOUS_BIND_ARGUMENT = "Room does not support ? as bind parameters. You must use" +
- " named bind arguments (e..g :argName)"
-
- val NOT_ONE_QUERY = "Must have exactly 1 query in @Query value"
-
- fun invalidQueryType(type: QueryType): String {
- return "$type query type is not supported yet. You can use:" +
- QueryType.SUPPORTED.joinToString(", ") { it.name }
- }
-
- fun cannotUseVariableIndices(name: String, position: Int) = "Cannot use variable indices." +
- " Use named parameters instead (e.g. WHERE name LIKE :nameArg and lastName LIKE " +
- ":lastName). Problem: $name at $position"
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/SqlParser.kt
deleted file mode 100644
index 0114a83..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/parser/SqlParser.kt
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (C) 2016 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.parser
-
-import android.arch.persistence.room.ColumnInfo
-import org.antlr.v4.runtime.ANTLRInputStream
-import org.antlr.v4.runtime.BaseErrorListener
-import org.antlr.v4.runtime.CommonTokenStream
-import org.antlr.v4.runtime.RecognitionException
-import org.antlr.v4.runtime.Recognizer
-import org.antlr.v4.runtime.tree.ParseTree
-import org.antlr.v4.runtime.tree.TerminalNode
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-
-@Suppress("FunctionName")
-class QueryVisitor(
- private val original: String,
- private val syntaxErrors: ArrayList<String>,
- statement: ParseTree,
- private val forRuntimeQuery: Boolean
-) : SQLiteBaseVisitor<Void?>() {
- private val bindingExpressions = arrayListOf<TerminalNode>()
- // table name alias mappings
- private val tableNames = mutableSetOf<Table>()
- private val withClauseNames = mutableSetOf<String>()
- private val queryType: QueryType
-
- init {
- queryType = (0 until statement.childCount).map {
- findQueryType(statement.getChild(it))
- }.filterNot { it == QueryType.UNKNOWN }.firstOrNull() ?: QueryType.UNKNOWN
-
- statement.accept(this)
- }
-
- private fun findQueryType(statement: ParseTree): QueryType {
- return when (statement) {
- is SQLiteParser.Factored_select_stmtContext,
- is SQLiteParser.Compound_select_stmtContext,
- is SQLiteParser.Select_stmtContext,
- is SQLiteParser.Simple_select_stmtContext ->
- QueryType.SELECT
-
- is SQLiteParser.Delete_stmt_limitedContext,
- is SQLiteParser.Delete_stmtContext ->
- QueryType.DELETE
-
- is SQLiteParser.Insert_stmtContext ->
- QueryType.INSERT
- is SQLiteParser.Update_stmtContext,
- is SQLiteParser.Update_stmt_limitedContext ->
- QueryType.UPDATE
- is TerminalNode -> when (statement.text) {
- "EXPLAIN" -> QueryType.EXPLAIN
- else -> QueryType.UNKNOWN
- }
- else -> QueryType.UNKNOWN
- }
- }
-
- override fun visitExpr(ctx: SQLiteParser.ExprContext): Void? {
- val bindParameter = ctx.BIND_PARAMETER()
- if (bindParameter != null) {
- bindingExpressions.add(bindParameter)
- }
- return super.visitExpr(ctx)
- }
-
- fun createParsedQuery(): ParsedQuery {
- return ParsedQuery(
- original = original,
- type = queryType,
- inputs = bindingExpressions.sortedBy { it.sourceInterval.a },
- tables = tableNames,
- syntaxErrors = syntaxErrors,
- runtimeQueryPlaceholder = forRuntimeQuery)
- }
-
- override fun visitCommon_table_expression(
- ctx: SQLiteParser.Common_table_expressionContext): Void? {
- val tableName = ctx.table_name()?.text
- if (tableName != null) {
- withClauseNames.add(unescapeIdentifier(tableName))
- }
- return super.visitCommon_table_expression(ctx)
- }
-
- override fun visitTable_or_subquery(ctx: SQLiteParser.Table_or_subqueryContext): Void? {
- val tableName = ctx.table_name()?.text
- if (tableName != null) {
- val tableAlias = ctx.table_alias()?.text
- if (tableName !in withClauseNames) {
- tableNames.add(Table(
- unescapeIdentifier(tableName),
- unescapeIdentifier(tableAlias ?: tableName)))
- }
- }
- return super.visitTable_or_subquery(ctx)
- }
-
- private fun unescapeIdentifier(text: String): String {
- val trimmed = text.trim()
- ESCAPE_LITERALS.forEach {
- if (trimmed.startsWith(it) && trimmed.endsWith(it)) {
- return unescapeIdentifier(trimmed.substring(1, trimmed.length - 1))
- }
- }
- return trimmed
- }
-
- companion object {
- private val ESCAPE_LITERALS = listOf("\"", "'", "`")
- }
-}
-
-class SqlParser {
- companion object {
- private val INVALID_IDENTIFIER_CHARS = arrayOf('`', '\"')
- fun parse(input: String): ParsedQuery {
- val inputStream = ANTLRInputStream(input)
- val lexer = SQLiteLexer(inputStream)
- val tokenStream = CommonTokenStream(lexer)
- val parser = SQLiteParser(tokenStream)
- val syntaxErrors = arrayListOf<String>()
- parser.addErrorListener(object : BaseErrorListener() {
- override fun syntaxError(
- recognizer: Recognizer<*, *>, offendingSymbol: Any,
- line: Int, charPositionInLine: Int, msg: String,
- e: RecognitionException?) {
- syntaxErrors.add(msg)
- }
- })
- try {
- val parsed = parser.parse()
- val statementList = parsed.sql_stmt_list()
- if (statementList.isEmpty()) {
- syntaxErrors.add(ParserErrors.NOT_ONE_QUERY)
- return ParsedQuery(input, QueryType.UNKNOWN, emptyList(), emptySet(),
- listOf(ParserErrors.NOT_ONE_QUERY), false)
- }
- val statements = statementList.first().children
- .filter { it is SQLiteParser.Sql_stmtContext }
- if (statements.size != 1) {
- syntaxErrors.add(ParserErrors.NOT_ONE_QUERY)
- }
- val statement = statements.first()
- return QueryVisitor(
- original = input,
- syntaxErrors = syntaxErrors,
- statement = statement,
- forRuntimeQuery = false).createParsedQuery()
- } catch (antlrError: RuntimeException) {
- return ParsedQuery(input, QueryType.UNKNOWN, emptyList(), emptySet(),
- listOf("unknown error while parsing $input : ${antlrError.message}"),
- false)
- }
- }
-
- fun isValidIdentifier(input: String): Boolean =
- input.isNotBlank() && INVALID_IDENTIFIER_CHARS.none { input.contains(it) }
-
- /**
- * creates a dummy select query for raw queries that queries the given list of tables.
- */
- fun rawQueryForTables(tableNames: Set<String>): ParsedQuery {
- return ParsedQuery(
- original = "raw query",
- type = QueryType.UNKNOWN,
- inputs = emptyList(),
- tables = tableNames.map { Table(name = it, alias = it) }.toSet(),
- syntaxErrors = emptyList(),
- runtimeQueryPlaceholder = true
- )
- }
- }
-}
-
-enum class QueryType {
- UNKNOWN,
- SELECT,
- DELETE,
- UPDATE,
- EXPLAIN,
- INSERT;
-
- companion object {
- // IF you change this, don't forget to update @Query documentation.
- val SUPPORTED = hashSetOf(SELECT, DELETE, UPDATE)
- }
-}
-
-enum class SQLTypeAffinity {
- NULL,
- TEXT,
- INTEGER,
- REAL,
- BLOB;
-
- fun getTypeMirrors(env: ProcessingEnvironment): List<TypeMirror>? {
- val typeUtils = env.typeUtils
- return when (this) {
- TEXT -> listOf(env.elementUtils.getTypeElement("java.lang.String").asType())
- INTEGER -> withBoxedTypes(env, TypeKind.INT, TypeKind.BYTE, TypeKind.CHAR,
- TypeKind.LONG, TypeKind.SHORT)
- REAL -> withBoxedTypes(env, TypeKind.DOUBLE, TypeKind.FLOAT)
- BLOB -> listOf(typeUtils.getArrayType(
- typeUtils.getPrimitiveType(TypeKind.BYTE)))
- else -> emptyList()
- }
- }
-
- private fun withBoxedTypes(env: ProcessingEnvironment, vararg primitives: TypeKind):
- List<TypeMirror> {
- return primitives.flatMap {
- val primitiveType = env.typeUtils.getPrimitiveType(it)
- listOf(primitiveType, env.typeUtils.boxedClass(primitiveType).asType())
- }
- }
-
- companion object {
- // converts from ColumnInfo#SQLiteTypeAffinity
- fun fromAnnotationValue(value: Int): SQLTypeAffinity? {
- return when (value) {
- ColumnInfo.BLOB -> BLOB
- ColumnInfo.INTEGER -> INTEGER
- ColumnInfo.REAL -> REAL
- ColumnInfo.TEXT -> TEXT
- else -> null
- }
- }
- }
-}
-
-enum class Collate {
- BINARY,
- NOCASE,
- RTRIM,
- LOCALIZED,
- UNICODE;
-
- companion object {
- fun fromAnnotationValue(value: Int): Collate? {
- return when (value) {
- ColumnInfo.BINARY -> BINARY
- ColumnInfo.NOCASE -> NOCASE
- ColumnInfo.RTRIM -> RTRIM
- ColumnInfo.LOCALIZED -> LOCALIZED
- ColumnInfo.UNICODE -> UNICODE
- else -> null
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/preconditions/Checks.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/preconditions/Checks.kt
deleted file mode 100644
index 588ce94..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/preconditions/Checks.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2016 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.preconditions
-
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.log.RLog
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
-import javax.lang.model.element.Element
-import kotlin.reflect.KClass
-
-/**
- * Similar to preconditions but element bound and just logs the error instead of throwing an
- * exception.
- * <p>
- * It is important for processing to continue when some errors happen so that we can generate as
- * much code as possible, leaving only the errors in javac output.
- */
-class Checks(private val logger: RLog) {
-
- fun check(predicate: Boolean, element: Element, errorMsg: String, vararg args: Any): Boolean {
- if (!predicate) {
- logger.e(element, errorMsg, args)
- }
- return predicate
- }
-
- fun hasAnnotation(element: Element, annotation: KClass<out Annotation>, errorMsg: String,
- vararg args: Any): Boolean {
- return if (!element.hasAnnotation(annotation)) {
- logger.e(element, errorMsg, args)
- false
- } else {
- true
- }
- }
-
- fun notUnbound(typeName: TypeName, element: Element, errorMsg: String,
- vararg args: Any): Boolean {
- // TODO support bounds cases like <T extends Foo> T bar()
- val failed = check(typeName !is TypeVariableName, element, errorMsg, args)
- if (typeName is ParameterizedTypeName) {
- val nestedFailure = typeName.typeArguments
- .map { notUnbound(it, element, errorMsg, args) }
- .any { it == true }
- return !(failed || nestedFailure)
- }
- return !failed
- }
-
- fun notBlank(value: String?, element: Element, msg: String, vararg args: Any): Boolean {
- return check(value != null && value.isNotBlank(), element, msg, args)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/Context.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/Context.kt
deleted file mode 100644
index def5107..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/Context.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.log.RLog
-import android.arch.persistence.room.preconditions.Checks
-import android.arch.persistence.room.processor.cache.Cache
-import android.arch.persistence.room.solver.TypeAdapterStore
-import android.arch.persistence.room.verifier.DatabaseVerifier
-import java.io.File
-import java.util.LinkedHashSet
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.Element
-import javax.lang.model.type.TypeMirror
-
-class Context private constructor(
- val processingEnv: ProcessingEnvironment,
- val logger: RLog,
- private val typeConverters: CustomConverterProcessor.ProcessResult,
- private val inheritedAdapterStore: TypeAdapterStore?,
- val cache: Cache) {
- val checker: Checks = Checks(logger)
- val COMMON_TYPES: Context.CommonTypes = Context.CommonTypes(processingEnv)
-
- val typeAdapterStore by lazy {
- if (inheritedAdapterStore != null) {
- TypeAdapterStore.copy(this, inheritedAdapterStore)
- } else {
- TypeAdapterStore.create(this, typeConverters.converters)
- }
- }
-
- // set when database and its entities are processed.
- var databaseVerifier: DatabaseVerifier? = null
-
- companion object {
- val ARG_OPTIONS by lazy {
- ProcessorOptions.values().map { it.argName }
- }
- }
-
- constructor(processingEnv: ProcessingEnvironment) : this(
- processingEnv = processingEnv,
- logger = RLog(RLog.ProcessingEnvMessager(processingEnv), emptySet(), null),
- typeConverters = CustomConverterProcessor.ProcessResult.EMPTY,
- inheritedAdapterStore = null,
- cache = Cache(null, LinkedHashSet(), emptySet())) {
- }
-
- class CommonTypes(val processingEnv: ProcessingEnvironment) {
- val STRING: TypeMirror by lazy {
- processingEnv.elementUtils.getTypeElement("java.lang.String").asType()
- }
- }
-
- val schemaOutFolder by lazy {
- val arg = processingEnv.options[ProcessorOptions.OPTION_SCHEMA_FOLDER.argName]
- if (arg?.isNotEmpty() ?: false) {
- File(arg)
- } else {
- null
- }
- }
-
- fun <T> collectLogs(handler: (Context) -> T): Pair<T, RLog.CollectingMessager> {
- val collector = RLog.CollectingMessager()
- val subContext = Context(processingEnv = processingEnv,
- logger = RLog(collector, logger.suppressedWarnings, logger.defaultElement),
- typeConverters = this.typeConverters,
- inheritedAdapterStore = typeAdapterStore,
- cache = cache)
- subContext.databaseVerifier = databaseVerifier
- val result = handler(subContext)
- return Pair(result, collector)
- }
-
- fun fork(element: Element): Context {
- val suppressedWarnings = SuppressWarningProcessor.getSuppressedWarnings(element)
- val processConvertersResult = CustomConverterProcessor.findConverters(this, element)
- val canReUseAdapterStore = processConvertersResult.classes.isEmpty()
- // order here is important since the sub context should give priority to new converters.
- val subTypeConverters = if (canReUseAdapterStore) {
- this.typeConverters
- } else {
- processConvertersResult + this.typeConverters
- }
- val subSuppressedWarnings = suppressedWarnings + logger.suppressedWarnings
- val subCache = Cache(cache, subTypeConverters.classes, subSuppressedWarnings)
- val subContext = Context(
- processingEnv = processingEnv,
- logger = RLog(logger.messager, subSuppressedWarnings, element),
- typeConverters = subTypeConverters,
- inheritedAdapterStore = if (canReUseAdapterStore) typeAdapterStore else null,
- cache = subCache)
- subContext.databaseVerifier = databaseVerifier
- return subContext
- }
-
- enum class ProcessorOptions(val argName: String) {
- OPTION_SCHEMA_FOLDER("room.schemaLocation")
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/CustomConverterProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/CustomConverterProcessor.kt
deleted file mode 100644
index 622621d..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/CustomConverterProcessor.kt
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.TypeConverter
-import android.arch.persistence.room.TypeConverters
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.hasAnyOf
-import android.arch.persistence.room.ext.toListOfClassTypes
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_BAD_RETURN_TYPE
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
-import android.arch.persistence.room.processor.ProcessorErrors
- .TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_BE_PUBLIC
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_RECEIVE_1_PARAM
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_UNBOUND_GENERIC
-import android.arch.persistence.room.solver.types.CustomTypeConverterWrapper
-import android.arch.persistence.room.vo.CustomTypeConverter
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import java.util.LinkedHashSet
-import javax.lang.model.element.Element
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-import javax.lang.model.util.ElementFilter
-
-/**
- * Processes classes that are referenced in TypeConverters annotations.
- */
-class CustomConverterProcessor(val context: Context, val element: TypeElement) {
- companion object {
- private val INVALID_RETURN_TYPES = setOf(TypeKind.ERROR, TypeKind.VOID, TypeKind.NONE)
- fun findConverters(context: Context, element: Element): ProcessResult {
- val annotation = MoreElements.getAnnotationMirror(element,
- TypeConverters::class.java).orNull()
- return annotation?.let {
- val classes = AnnotationMirrors.getAnnotationValue(annotation, "value")
- ?.toListOfClassTypes()
- ?.filter {
- MoreTypes.isType(it)
- }?.mapTo(LinkedHashSet(), { it }) ?: LinkedHashSet<TypeMirror>()
- val converters = classes
- .flatMap {
- CustomConverterProcessor(context, MoreTypes.asTypeElement(it))
- .process()
- }
- converters.let {
- reportDuplicates(context, converters)
- }
- ProcessResult(classes, converters.map(::CustomTypeConverterWrapper))
- } ?: ProcessResult.EMPTY
- }
-
- fun reportDuplicates(context: Context, converters: List<CustomTypeConverter>) {
- val groupedByFrom = converters.groupBy { it.from.typeName() }
- groupedByFrom.forEach {
- it.value.groupBy { it.to.typeName() }.forEach {
- if (it.value.size > 1) {
- it.value.forEach { converter ->
- context.logger.e(converter.method, ProcessorErrors
- .duplicateTypeConverters(it.value.minus(converter)))
- }
- }
- }
- }
- }
- }
-
- fun process(): List<CustomTypeConverter> {
- // using element utils instead of MoreElements to include statics.
- val methods = ElementFilter
- .methodsIn(context.processingEnv.elementUtils.getAllMembers(element))
- val declaredType = MoreTypes.asDeclared(element.asType())
- val converterMethods = methods.filter {
- it.hasAnnotation(TypeConverter::class)
- }
- context.checker.check(converterMethods.isNotEmpty(), element, TYPE_CONVERTER_EMPTY_CLASS)
- val allStatic = converterMethods.all { it.modifiers.contains(Modifier.STATIC) }
- val constructors = ElementFilter.constructorsIn(
- context.processingEnv.elementUtils.getAllMembers(element))
- context.checker.check(allStatic || constructors.isEmpty() || constructors.any {
- it.parameters.isEmpty()
- }, element, TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
- return converterMethods.map {
- processMethod(declaredType, it)
- }.filterNotNull()
- }
-
- private fun processMethod(
- container: DeclaredType, methodElement: ExecutableElement): CustomTypeConverter? {
- val asMember = context.processingEnv.typeUtils.asMemberOf(container, methodElement)
- val executableType = MoreTypes.asExecutable(asMember)
- val returnType = executableType.returnType
- val invalidReturnType = INVALID_RETURN_TYPES.contains(returnType.kind)
- context.checker.check(methodElement.hasAnyOf(Modifier.PUBLIC), methodElement,
- TYPE_CONVERTER_MUST_BE_PUBLIC)
- if (invalidReturnType) {
- context.logger.e(methodElement, TYPE_CONVERTER_BAD_RETURN_TYPE)
- return null
- }
- val returnTypeName = returnType.typeName()
- context.checker.notUnbound(returnTypeName, methodElement,
- TYPE_CONVERTER_UNBOUND_GENERIC)
- val params = methodElement.parameters
- if (params.size != 1) {
- context.logger.e(methodElement, TYPE_CONVERTER_MUST_RECEIVE_1_PARAM)
- return null
- }
- val param = params.map {
- MoreTypes.asMemberOf(context.processingEnv.typeUtils, container, it)
- }.first()
- context.checker.notUnbound(param.typeName(), params[0], TYPE_CONVERTER_UNBOUND_GENERIC)
- return CustomTypeConverter(container, methodElement, param, returnType)
- }
-
- /**
- * Order of classes is important hence they are a LinkedHashSet not a set.
- */
- open class ProcessResult(
- val classes: LinkedHashSet<TypeMirror>,
- val converters: List<CustomTypeConverterWrapper>
- ) {
- object EMPTY : ProcessResult(LinkedHashSet(), emptyList())
- operator fun plus(other: ProcessResult): ProcessResult {
- val newClasses = LinkedHashSet<TypeMirror>()
- newClasses.addAll(classes)
- newClasses.addAll(other.classes)
- return ProcessResult(newClasses, converters + other.converters)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
deleted file mode 100644
index 0a622c9..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Delete
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.RawQuery
-import android.arch.persistence.room.SkipQueryVerification
-import android.arch.persistence.room.Transaction
-import android.arch.persistence.room.Update
-import android.arch.persistence.room.ext.findKotlinDefaultImpl
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.hasAnyOf
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.verifier.DatabaseVerifier
-import android.arch.persistence.room.vo.Dao
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-
-class DaoProcessor(baseContext: Context, val element: TypeElement, val dbType: DeclaredType,
- val dbVerifier: DatabaseVerifier?) {
- val context = baseContext.fork(element)
-
- companion object {
- val PROCESSED_ANNOTATIONS = listOf(Insert::class, Delete::class, Query::class,
- Update::class, RawQuery::class)
- }
-
- fun process(): Dao {
- context.checker.hasAnnotation(element, android.arch.persistence.room.Dao::class,
- ProcessorErrors.DAO_MUST_BE_ANNOTATED_WITH_DAO)
- context.checker.check(element.hasAnyOf(ABSTRACT) || element.kind == ElementKind.INTERFACE,
- element, ProcessorErrors.DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE)
-
- val declaredType = MoreTypes.asDeclared(element.asType())
- val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
- val methods = allMembers
- .filter {
- it.hasAnyOf(ABSTRACT) && it.kind == ElementKind.METHOD
- && it.findKotlinDefaultImpl(context.processingEnv.typeUtils) == null
- }.map {
- MoreElements.asExecutable(it)
- }.groupBy { method ->
- context.checker.check(
- PROCESSED_ANNOTATIONS.count { method.hasAnnotation(it) } == 1, method,
- ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD
- )
- if (method.hasAnnotation(Query::class)) {
- Query::class
- } else if (method.hasAnnotation(Insert::class)) {
- Insert::class
- } else if (method.hasAnnotation(Delete::class)) {
- Delete::class
- } else if (method.hasAnnotation(Update::class)) {
- Update::class
- } else if (method.hasAnnotation(RawQuery::class)) {
- RawQuery::class
- } else {
- Any::class
- }
- }
- val processorVerifier = if (element.hasAnnotation(SkipQueryVerification::class) ||
- element.hasAnnotation(RawQuery::class)) {
- null
- } else {
- dbVerifier
- }
-
- val queryMethods = methods[Query::class]?.map {
- QueryMethodProcessor(
- baseContext = context,
- containing = declaredType,
- executableElement = it,
- dbVerifier = processorVerifier).process()
- } ?: emptyList()
-
- val rawQueryMethods = methods[RawQuery::class]?.map {
- RawQueryMethodProcessor(
- baseContext = context,
- containing = declaredType,
- executableElement = it
- ).process()
- } ?: emptyList()
-
- val insertionMethods = methods[Insert::class]?.map {
- InsertionMethodProcessor(
- baseContext = context,
- containing = declaredType,
- executableElement = it).process()
- } ?: emptyList()
-
- val deletionMethods = methods[Delete::class]?.map {
- DeletionMethodProcessor(
- baseContext = context,
- containing = declaredType,
- executableElement = it).process()
- } ?: emptyList()
-
- val updateMethods = methods[Update::class]?.map {
- UpdateMethodProcessor(
- baseContext = context,
- containing = declaredType,
- executableElement = it).process()
- } ?: emptyList()
-
- val transactionMethods = allMembers.filter { member ->
- member.hasAnnotation(Transaction::class)
- && member.kind == ElementKind.METHOD
- && PROCESSED_ANNOTATIONS.none { member.hasAnnotation(it) }
- }.map {
- TransactionMethodProcessor(
- baseContext = context,
- containing = declaredType,
- executableElement = MoreElements.asExecutable(it)).process()
- }
-
- val constructors = allMembers
- .filter { it.kind == ElementKind.CONSTRUCTOR }
- .map { MoreElements.asExecutable(it) }
- val typeUtils = context.processingEnv.typeUtils
- val goodConstructor = constructors
- .filter {
- it.parameters.size == 1
- && typeUtils.isAssignable(dbType, it.parameters[0].asType())
- }
- .firstOrNull()
- val constructorParamType = if (goodConstructor != null) {
- goodConstructor.parameters[0].asType().typeName()
- } else {
- validateEmptyConstructor(constructors)
- null
- }
-
- context.checker.check(methods[Any::class] == null, element,
- ProcessorErrors.ABSTRACT_METHOD_IN_DAO_MISSING_ANY_ANNOTATION)
-
- val type = TypeName.get(declaredType)
- context.checker.notUnbound(type, element,
- ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_DAO_CLASSES)
-
- return Dao(element = element,
- type = declaredType,
- queryMethods = queryMethods,
- rawQueryMethods = rawQueryMethods,
- insertionMethods = insertionMethods,
- deletionMethods = deletionMethods,
- updateMethods = updateMethods,
- transactionMethods = transactionMethods,
- constructorParamType = constructorParamType)
- }
-
- private fun validateEmptyConstructor(constructors: List<ExecutableElement>) {
- if (constructors.isNotEmpty() && constructors.all { it.parameters.isNotEmpty() }) {
- context.logger.e(element, ProcessorErrors.daoMustHaveMatchingConstructor(
- element.toString(), dbType.toString()))
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DatabaseProcessor.kt
deleted file mode 100644
index 30acbf4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DatabaseProcessor.kt
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.SkipQueryVerification
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.getAsBoolean
-import android.arch.persistence.room.ext.getAsInt
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.hasAnyOf
-import android.arch.persistence.room.ext.toListOfClassTypes
-import android.arch.persistence.room.verifier.DatabaseVerifier
-import android.arch.persistence.room.vo.Dao
-import android.arch.persistence.room.vo.DaoMethod
-import android.arch.persistence.room.vo.Database
-import android.arch.persistence.room.vo.Entity
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.AnnotationMirror
-import javax.lang.model.element.Element
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeMirror
-
-class DatabaseProcessor(baseContext: Context, val element: TypeElement) {
- val context = baseContext.fork(element)
-
- val baseClassElement: TypeMirror by lazy {
- context.processingEnv.elementUtils.getTypeElement(
- RoomTypeNames.ROOM_DB.packageName() + "." + RoomTypeNames.ROOM_DB.simpleName())
- .asType()
- }
-
- fun process(): Database {
- try {
- return doProcess()
- } finally {
- context.databaseVerifier?.closeConnection(context)
- }
- }
-
- private fun doProcess(): Database {
- val dbAnnotation = MoreElements
- .getAnnotationMirror(element, android.arch.persistence.room.Database::class.java)
- .orNull()
- val entities = processEntities(dbAnnotation, element)
- validateUniqueTableNames(element, entities)
- validateForeignKeys(element, entities)
-
- val extendsRoomDb = context.processingEnv.typeUtils.isAssignable(
- MoreElements.asType(element).asType(), baseClassElement)
- context.checker.check(extendsRoomDb, element, ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
-
- val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
-
- val dbVerifier = if (element.hasAnnotation(SkipQueryVerification::class)) {
- null
- } else {
- DatabaseVerifier.create(context, element, entities)
- }
- context.databaseVerifier = dbVerifier
-
- val declaredType = MoreTypes.asDeclared(element.asType())
- val daoMethods = allMembers.filter {
- it.hasAnyOf(Modifier.ABSTRACT) && it.kind == ElementKind.METHOD
- }.filterNot {
- // remove methods that belong to room
- val containing = it.enclosingElement
- MoreElements.isType(containing) &&
- TypeName.get(containing.asType()) == RoomTypeNames.ROOM_DB
- }.map {
- val executable = MoreElements.asExecutable(it)
- // TODO when we add support for non Dao return types (e.g. database), this code needs
- // to change
- val daoType = MoreTypes.asTypeElement(executable.returnType)
- val dao = DaoProcessor(context, daoType, declaredType, dbVerifier).process()
- DaoMethod(executable, executable.simpleName.toString(), dao)
- }
- validateUniqueDaoClasses(element, daoMethods, entities)
- validateUniqueIndices(element, entities)
- val version = AnnotationMirrors.getAnnotationValue(dbAnnotation, "version")
- .getAsInt(1)!!.toInt()
- val exportSchema = AnnotationMirrors.getAnnotationValue(dbAnnotation, "exportSchema")
- .getAsBoolean(true)
-
- val hasForeignKeys = entities.any { it.foreignKeys.isNotEmpty() }
-
- val database = Database(
- version = version,
- element = element,
- type = MoreElements.asType(element).asType(),
- entities = entities,
- daoMethods = daoMethods,
- exportSchema = exportSchema,
- enableForeignKeys = hasForeignKeys)
- return database
- }
-
- private fun validateForeignKeys(element: TypeElement, entities: List<Entity>) {
- val byTableName = entities.associateBy { it.tableName }
- entities.forEach { entity ->
- entity.foreignKeys.forEach foreignKeyLoop@ { foreignKey ->
- val parent = byTableName[foreignKey.parentTable]
- if (parent == null) {
- context.logger.e(element, ProcessorErrors
- .foreignKeyMissingParentEntityInDatabase(foreignKey.parentTable,
- entity.element.qualifiedName.toString()))
- return@foreignKeyLoop
- }
- val parentFields = foreignKey.parentColumns.map { columnName ->
- val parentField = parent.fields.find {
- it.columnName == columnName
- }
- if (parentField == null) {
- context.logger.e(entity.element,
- ProcessorErrors.foreignKeyParentColumnDoesNotExist(
- parentEntity = parent.element.qualifiedName.toString(),
- missingColumn = columnName,
- allColumns = parent.fields.map { it.columnName }))
- }
- parentField
- }.filterNotNull()
- if (parentFields.size != foreignKey.parentColumns.size) {
- return@foreignKeyLoop
- }
- // ensure that it is indexed in the parent
- if (!parent.isUnique(foreignKey.parentColumns)) {
- context.logger.e(parent.element, ProcessorErrors
- .foreignKeyMissingIndexInParent(
- parentEntity = parent.element.qualifiedName.toString(),
- childEntity = entity.element.qualifiedName.toString(),
- parentColumns = foreignKey.parentColumns,
- childColumns = foreignKey.childFields
- .map { it.columnName }))
- return@foreignKeyLoop
- }
- }
- }
- }
-
- private fun validateUniqueIndices(element: TypeElement, entities: List<Entity>) {
- entities
- .flatMap { entity ->
- // associate each index with its entity
- entity.indices.map { Pair(it.name, entity) }
- }
- .groupBy { it.first } // group by index name
- .filter { it.value.size > 1 } // get the ones with duplicate names
- .forEach {
- // do not report duplicates from the same entity
- if (it.value.distinctBy { it.second.typeName }.size > 1) {
- context.logger.e(element,
- ProcessorErrors.duplicateIndexInDatabase(it.key,
- it.value.map { "${it.second.typeName} > ${it.first}" }))
- }
- }
- }
-
- private fun validateUniqueDaoClasses(dbElement: TypeElement, daoMethods: List<DaoMethod>,
- entities: List<Entity>) {
- val entityTypeNames = entities.map { it.typeName }.toSet()
- daoMethods.groupBy { it.dao.typeName }
- .forEach {
- if (it.value.size > 1) {
- val error = ProcessorErrors.duplicateDao(it.key, it.value.map { it.name })
- it.value.forEach { daoMethod ->
- context.logger.e(daoMethod.element,
- ProcessorErrors.DAO_METHOD_CONFLICTS_WITH_OTHERS)
- }
- // also report the full error for the database
- context.logger.e(dbElement, error)
- }
- }
- val check = fun(element: Element, dao: Dao,
- typeName: TypeName?) {
- typeName?.let {
- if (!entityTypeNames.contains(typeName)) {
- context.logger.e(element,
- ProcessorErrors.shortcutEntityIsNotInDatabase(
- database = dbElement.qualifiedName.toString(),
- dao = dao.typeName.toString(),
- entity = typeName.toString()
- ))
- }
- }
- }
- daoMethods.forEach { daoMethod ->
- daoMethod.dao.shortcutMethods.forEach { method ->
- method.entities.forEach {
- check(method.element, daoMethod.dao, it.value.typeName)
- }
- }
- daoMethod.dao.insertionMethods.forEach { method ->
- method.entities.forEach {
- check(method.element, daoMethod.dao, it.value.typeName)
- }
- }
- }
- }
-
- private fun validateUniqueTableNames(dbElement: TypeElement, entities: List<Entity>) {
- entities
- .groupBy {
- it.tableName.toLowerCase()
- }.filter {
- it.value.size > 1
- }.forEach { byTableName ->
- val error = ProcessorErrors.duplicateTableNames(byTableName.key,
- byTableName.value.map { it.typeName.toString() })
- // report it for each of them and the database to make it easier
- // for the developer
- byTableName.value.forEach { entity ->
- context.logger.e(entity.element, error)
- }
- context.logger.e(dbElement, error)
- }
- }
-
- private fun processEntities(dbAnnotation: AnnotationMirror?, element: TypeElement):
- List<Entity> {
- if (!context.checker.check(dbAnnotation != null, element,
- ProcessorErrors.DATABASE_MUST_BE_ANNOTATED_WITH_DATABASE)) {
- return listOf()
- }
-
- val entityList = AnnotationMirrors.getAnnotationValue(dbAnnotation, "entities")
- val listOfTypes = entityList.toListOfClassTypes()
- context.checker.check(listOfTypes.isNotEmpty(), element,
- ProcessorErrors.DATABASE_ANNOTATION_MUST_HAVE_LIST_OF_ENTITIES)
- return listOfTypes.map {
- EntityProcessor(context, MoreTypes.asTypeElement(it)).process()
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DeletionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DeletionMethodProcessor.kt
deleted file mode 100644
index 4812306..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DeletionMethodProcessor.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Delete
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.vo.DeletionMethod
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-
-class DeletionMethodProcessor(baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement) {
- val context = baseContext.fork(executableElement)
-
- fun process(): DeletionMethod {
- val delegate = ShortcutMethodProcessor(context, containing, executableElement)
- delegate.extractAnnotation(Delete::class, ProcessorErrors.MISSING_DELETE_ANNOTATION)
-
- val returnTypeName = delegate.extractReturnType().typeName()
- context.checker.check(
- returnTypeName == TypeName.VOID || returnTypeName == TypeName.INT,
- executableElement,
- ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
- )
-
- val (entities, params) = delegate.extractParams(
- missingParamError = ProcessorErrors
- .DELETION_MISSING_PARAMS
- )
-
- return DeletionMethod(
- element = delegate.executableElement,
- name = delegate.executableElement.simpleName.toString(),
- entities = entities,
- returnCount = returnTypeName == TypeName.INT,
- parameters = params
- )
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt
deleted file mode 100644
index 3859028..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/EntityProcessor.kt
+++ /dev/null
@@ -1,589 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.ext.getAsBoolean
-import android.arch.persistence.room.ext.getAsInt
-import android.arch.persistence.room.ext.getAsString
-import android.arch.persistence.room.ext.getAsStringList
-import android.arch.persistence.room.ext.toType
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.parser.SqlParser
-import android.arch.persistence.room.processor.ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY
-import android.arch.persistence.room.processor.ProcessorErrors.RELATION_IN_ENTITY
-import android.arch.persistence.room.processor.cache.Cache
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.ForeignKey
-import android.arch.persistence.room.vo.ForeignKeyAction
-import android.arch.persistence.room.vo.Index
-import android.arch.persistence.room.vo.Pojo
-import android.arch.persistence.room.vo.PrimaryKey
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.AnnotationMirrors.getAnnotationValue
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.AnnotationMirror
-import javax.lang.model.element.AnnotationValue
-import javax.lang.model.element.Name
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-import javax.lang.model.util.SimpleAnnotationValueVisitor6
-
-class EntityProcessor(baseContext: Context,
- val element: TypeElement,
- private val referenceStack: LinkedHashSet<Name> = LinkedHashSet()) {
- val context = baseContext.fork(element)
-
- fun process(): Entity {
- return context.cache.entities.get(Cache.EntityKey(element), {
- doProcess()
- })
- }
- private fun doProcess(): Entity {
- context.checker.hasAnnotation(element, android.arch.persistence.room.Entity::class,
- ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY)
- val pojo = PojoProcessor(
- baseContext = context,
- element = element,
- bindingScope = FieldProcessor.BindingScope.TWO_WAY,
- parent = null,
- referenceStack = referenceStack).process()
- context.checker.check(pojo.relations.isEmpty(), element, RELATION_IN_ENTITY)
- val annotation = MoreElements.getAnnotationMirror(element,
- android.arch.persistence.room.Entity::class.java).orNull()
- val tableName: String
- val entityIndices: List<IndexInput>
- val foreignKeyInputs: List<ForeignKeyInput>
- val inheritSuperIndices: Boolean
- if (annotation != null) {
- tableName = extractTableName(element, annotation)
- entityIndices = extractIndices(annotation, tableName)
- inheritSuperIndices = AnnotationMirrors
- .getAnnotationValue(annotation, "inheritSuperIndices").getAsBoolean(false)
- foreignKeyInputs = extractForeignKeys(annotation)
- } else {
- tableName = element.simpleName.toString()
- foreignKeyInputs = emptyList()
- entityIndices = emptyList()
- inheritSuperIndices = false
- }
- context.checker.notBlank(tableName, element,
- ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
-
- val fieldIndices = pojo.fields
- .filter { it.indexed }
- .map {
- if (it.parent != null) {
- it.indexed = false
- context.logger.w(Warning.INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED, it.element,
- ProcessorErrors.droppedEmbeddedFieldIndex(
- it.getPath(), element.qualifiedName.toString()))
- null
- } else if (it.element.enclosingElement != element && !inheritSuperIndices) {
- it.indexed = false
- context.logger.w(Warning.INDEX_FROM_PARENT_FIELD_IS_DROPPED,
- ProcessorErrors.droppedSuperClassFieldIndex(
- it.columnName, element.toString(),
- it.element.enclosingElement.toString()
- ))
- null
- } else {
- IndexInput(
- name = createIndexName(listOf(it.columnName), tableName),
- unique = false,
- columnNames = listOf(it.columnName)
- )
- }
- }.filterNotNull()
- val superIndices = loadSuperIndices(element.superclass, tableName, inheritSuperIndices)
- val indexInputs = entityIndices + fieldIndices + superIndices
- val indices = validateAndCreateIndices(indexInputs, pojo)
-
- val primaryKey = findAndValidatePrimaryKey(pojo.fields, pojo.embeddedFields)
- val affinity = primaryKey.fields.firstOrNull()?.affinity ?: SQLTypeAffinity.TEXT
- context.checker.check(
- !primaryKey.autoGenerateId || affinity == SQLTypeAffinity.INTEGER,
- primaryKey.fields.firstOrNull()?.element ?: element,
- ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT
- )
-
- val entityForeignKeys = validateAndCreateForeignKeyReferences(foreignKeyInputs, pojo)
- checkIndicesForForeignKeys(entityForeignKeys, primaryKey, indices)
-
- context.checker.check(SqlParser.isValidIdentifier(tableName), element,
- ProcessorErrors.INVALID_TABLE_NAME)
- pojo.fields.forEach {
- context.checker.check(SqlParser.isValidIdentifier(it.columnName), it.element,
- ProcessorErrors.INVALID_COLUMN_NAME)
- }
-
- val entity = Entity(element = element,
- tableName = tableName,
- type = pojo.type,
- fields = pojo.fields,
- embeddedFields = pojo.embeddedFields,
- indices = indices,
- primaryKey = primaryKey,
- foreignKeys = entityForeignKeys,
- constructor = pojo.constructor)
-
- return entity
- }
-
- private fun checkIndicesForForeignKeys(entityForeignKeys: List<ForeignKey>,
- primaryKey: PrimaryKey,
- indices: List<Index>) {
- fun covers(columnNames: List<String>, fields: List<Field>): Boolean =
- fields.size >= columnNames.size && columnNames.withIndex().all {
- fields[it.index].columnName == it.value
- }
-
- entityForeignKeys.forEach { fKey ->
- val columnNames = fKey.childFields.map { it.columnName }
- val exists = covers(columnNames, primaryKey.fields) || indices.any { index ->
- covers(columnNames, index.fields)
- }
- if (!exists) {
- if (columnNames.size == 1) {
- context.logger.w(Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element,
- ProcessorErrors.foreignKeyMissingIndexInChildColumn(columnNames[0]))
- } else {
- context.logger.w(Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element,
- ProcessorErrors.foreignKeyMissingIndexInChildColumns(columnNames))
- }
- }
- }
- }
-
- /**
- * Does a validation on foreign keys except the parent table's columns.
- */
- private fun validateAndCreateForeignKeyReferences(foreignKeyInputs: List<ForeignKeyInput>,
- pojo: Pojo): List<ForeignKey> {
- return foreignKeyInputs.map {
- if (it.onUpdate == null) {
- context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
- return@map null
- }
- if (it.onDelete == null) {
- context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
- return@map null
- }
- if (it.childColumns.isEmpty()) {
- context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
- return@map null
- }
- if (it.parentColumns.isEmpty()) {
- context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
- return@map null
- }
- if (it.childColumns.size != it.parentColumns.size) {
- context.logger.e(element, ProcessorErrors.foreignKeyColumnNumberMismatch(
- it.childColumns, it.parentColumns
- ))
- return@map null
- }
- val parentElement = try {
- MoreTypes.asElement(it.parent) as TypeElement
- } catch (noClass: IllegalArgumentException) {
- context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT)
- return@map null
- }
- val parentAnnotation = MoreElements.getAnnotationMirror(parentElement,
- android.arch.persistence.room.Entity::class.java).orNull()
- if (parentAnnotation == null) {
- context.logger.e(element,
- ProcessorErrors.foreignKeyNotAnEntity(parentElement.toString()))
- return@map null
- }
- val tableName = extractTableName(parentElement, parentAnnotation)
- val fields = it.childColumns.map { columnName ->
- val field = pojo.fields.find { it.columnName == columnName }
- if (field == null) {
- context.logger.e(pojo.element,
- ProcessorErrors.foreignKeyChildColumnDoesNotExist(columnName,
- pojo.fields.map { it.columnName }))
- }
- field
- }.filterNotNull()
- if (fields.size != it.childColumns.size) {
- return@map null
- }
- ForeignKey(
- parentTable = tableName,
- childFields = fields,
- parentColumns = it.parentColumns,
- onDelete = it.onDelete,
- onUpdate = it.onUpdate,
- deferred = it.deferred
- )
- }.filterNotNull()
- }
-
- private fun findAndValidatePrimaryKey(
- fields: List<Field>, embeddedFields: List<EmbeddedField>): PrimaryKey {
- val candidates = collectPrimaryKeysFromEntityAnnotations(element, fields) +
- collectPrimaryKeysFromPrimaryKeyAnnotations(fields) +
- collectPrimaryKeysFromEmbeddedFields(embeddedFields)
-
- context.checker.check(candidates.isNotEmpty(), element, ProcessorErrors.MISSING_PRIMARY_KEY)
-
- // 1. If a key is not autogenerated, but is Primary key or is part of Primary key we
- // force the @NonNull annotation. If the key is a single Primary Key, Integer or Long, we
- // don't force the @NonNull annotation since SQLite will automatically generate IDs.
- // 2. If a key is autogenerate, we generate NOT NULL in table spec, but we don't require
- // @NonNull annotation on the field itself.
- candidates.filter { candidate -> !candidate.autoGenerateId }
- .map { candidate ->
- candidate.fields.map { field ->
- if (candidate.fields.size > 1 ||
- (candidate.fields.size == 1
- && field.affinity != SQLTypeAffinity.INTEGER)) {
- context.checker.check(field.nonNull, field.element,
- ProcessorErrors.primaryKeyNull(field.getPath()))
- // Validate parents for nullability
- var parent = field.parent
- while (parent != null) {
- val parentField = parent.field
- context.checker.check(parentField.nonNull,
- parentField.element,
- ProcessorErrors.primaryKeyNull(parentField.getPath()))
- parent = parentField.parent
- }
- }
- }
- }
-
- if (candidates.size == 1) {
- // easy :)
- return candidates.first()
- }
-
- return choosePrimaryKey(candidates, element)
- }
-
- /**
- * Check fields for @PrimaryKey.
- */
- private fun collectPrimaryKeysFromPrimaryKeyAnnotations(fields: List<Field>): List<PrimaryKey> {
- return fields.map { field ->
- MoreElements.getAnnotationMirror(field.element,
- android.arch.persistence.room.PrimaryKey::class.java).orNull()?.let {
- if (field.parent != null) {
- // the field in the entity that contains this error.
- val grandParentField = field.parent.mRootParent.field.element
- // bound for entity.
- context.fork(grandParentField).logger.w(
- Warning.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED,
- grandParentField,
- ProcessorErrors.embeddedPrimaryKeyIsDropped(
- element.qualifiedName.toString(), field.name))
- null
- } else {
- PrimaryKey(declaredIn = field.element.enclosingElement,
- fields = listOf(field),
- autoGenerateId = AnnotationMirrors
- .getAnnotationValue(it, "autoGenerate")
- .getAsBoolean(false))
- }
- }
- }.filterNotNull()
- }
-
- /**
- * Check classes for @Entity(primaryKeys = ?).
- */
- private fun collectPrimaryKeysFromEntityAnnotations(
- typeElement: TypeElement, availableFields: List<Field>): List<PrimaryKey> {
- val myPkeys = MoreElements.getAnnotationMirror(typeElement,
- android.arch.persistence.room.Entity::class.java).orNull()?.let {
- val primaryKeyColumns = AnnotationMirrors.getAnnotationValue(it, "primaryKeys")
- .getAsStringList()
- if (primaryKeyColumns.isEmpty()) {
- emptyList<PrimaryKey>()
- } else {
- val fields = primaryKeyColumns.map { pKeyColumnName ->
- val field = availableFields.firstOrNull { it.columnName == pKeyColumnName }
- context.checker.check(field != null, typeElement,
- ProcessorErrors.primaryKeyColumnDoesNotExist(pKeyColumnName,
- availableFields.map { it.columnName }))
- field
- }.filterNotNull()
- listOf(PrimaryKey(declaredIn = typeElement,
- fields = fields,
- autoGenerateId = false))
- }
- } ?: emptyList<PrimaryKey>()
- // checks supers.
- val mySuper = typeElement.superclass
- val superPKeys = if (mySuper != null && mySuper.kind != TypeKind.NONE) {
- // my super cannot see my fields so remove them.
- val remainingFields = availableFields.filterNot {
- it.element.enclosingElement == typeElement
- }
- collectPrimaryKeysFromEntityAnnotations(
- MoreTypes.asTypeElement(mySuper), remainingFields)
- } else {
- emptyList()
- }
- return superPKeys + myPkeys
- }
-
- private fun collectPrimaryKeysFromEmbeddedFields(
- embeddedFields: List<EmbeddedField>): List<PrimaryKey> {
- return embeddedFields.map { embeddedField ->
- MoreElements.getAnnotationMirror(embeddedField.field.element,
- android.arch.persistence.room.PrimaryKey::class.java).orNull()?.let {
- val autoGenerate = AnnotationMirrors
- .getAnnotationValue(it, "autoGenerate").getAsBoolean(false)
- context.checker.check(!autoGenerate || embeddedField.pojo.fields.size == 1,
- embeddedField.field.element,
- ProcessorErrors.AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS)
- PrimaryKey(declaredIn = embeddedField.field.element.enclosingElement,
- fields = embeddedField.pojo.fields,
- autoGenerateId = autoGenerate)
- }
- }.filterNotNull()
- }
-
- // start from my element and check if anywhere in the list we can find the only well defined
- // pkey, if so, use it.
- private fun choosePrimaryKey(
- candidates: List<PrimaryKey>, typeElement: TypeElement): PrimaryKey {
- // If 1 of these primary keys is declared in this class, then it is the winner. Just print
- // a note for the others.
- // If 0 is declared, check the parent.
- // If more than 1 primary key is declared in this class, it is an error.
- val myPKeys = candidates.filter { candidate ->
- candidate.declaredIn == typeElement
- }
- return if (myPKeys.size == 1) {
- // just note, this is not worth an error or warning
- (candidates - myPKeys).forEach {
- context.logger.d(element,
- "${it.toHumanReadableString()} is" +
- " overridden by ${myPKeys.first().toHumanReadableString()}")
- }
- myPKeys.first()
- } else if (myPKeys.isEmpty()) {
- // i have not declared anything, delegate to super
- val mySuper = typeElement.superclass
- if (mySuper != null && mySuper.kind != TypeKind.NONE) {
- return choosePrimaryKey(candidates, MoreTypes.asTypeElement(mySuper))
- }
- PrimaryKey.MISSING
- } else {
- context.logger.e(element, ProcessorErrors.multiplePrimaryKeyAnnotations(
- myPKeys.map(PrimaryKey::toHumanReadableString)))
- PrimaryKey.MISSING
- }
- }
-
- private fun validateAndCreateIndices(
- inputs: List<IndexInput>, pojo: Pojo): List<Index> {
- // check for columns
- val indices = inputs.map { input ->
- context.checker.check(input.columnNames.isNotEmpty(), element,
- INDEX_COLUMNS_CANNOT_BE_EMPTY)
- val fields = input.columnNames.map { columnName ->
- val field = pojo.fields.firstOrNull {
- it.columnName == columnName
- }
- context.checker.check(field != null, element,
- ProcessorErrors.indexColumnDoesNotExist(
- columnName, pojo.fields.map { it.columnName }
- ))
- field
- }.filterNotNull()
- if (fields.isEmpty()) {
- null
- } else {
- Index(name = input.name, unique = input.unique, fields = fields)
- }
- }.filterNotNull()
-
- // check for duplicate indices
- indices
- .groupBy { it.name }
- .filter { it.value.size > 1 }
- .forEach {
- context.logger.e(element, ProcessorErrors.duplicateIndexInEntity(it.key))
- }
-
- // see if any embedded field is an entity with indices, if so, report a warning
- pojo.embeddedFields.forEach { embedded ->
- val embeddedElement = embedded.pojo.element
- val subEntityAnnotation = MoreElements.getAnnotationMirror(embeddedElement,
- android.arch.persistence.room.Entity::class.java).orNull()
- subEntityAnnotation?.let {
- val subIndices = extractIndices(subEntityAnnotation, "")
- if (subIndices.isNotEmpty()) {
- context.logger.w(Warning.INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED,
- embedded.field.element, ProcessorErrors.droppedEmbeddedIndex(
- entityName = embedded.pojo.typeName.toString(),
- fieldPath = embedded.field.getPath(),
- grandParent = element.qualifiedName.toString()))
- }
- }
- }
- return indices
- }
-
- // check if parent is an Entity, if so, report its annotation indices
- private fun loadSuperIndices(
- typeMirror: TypeMirror?, tableName: String, inherit: Boolean): List<IndexInput> {
- if (typeMirror == null || typeMirror.kind == TypeKind.NONE) {
- return emptyList()
- }
- val parentElement = MoreTypes.asTypeElement(typeMirror)
- val myIndices = MoreElements.getAnnotationMirror(parentElement,
- android.arch.persistence.room.Entity::class.java).orNull()?.let { annotation ->
- val indices = extractIndices(annotation, tableName = "super")
- if (indices.isEmpty()) {
- emptyList()
- } else if (inherit) {
- // rename them
- indices.map {
- IndexInput(
- name = createIndexName(it.columnNames, tableName),
- unique = it.unique,
- columnNames = it.columnNames)
- }
- } else {
- context.logger.w(Warning.INDEX_FROM_PARENT_IS_DROPPED,
- parentElement,
- ProcessorErrors.droppedSuperClassIndex(
- childEntity = element.qualifiedName.toString(),
- superEntity = parentElement.qualifiedName.toString()))
- emptyList()
- }
- } ?: emptyList()
- return myIndices + loadSuperIndices(parentElement.superclass, tableName, inherit)
- }
-
- companion object {
- fun extractTableName(element: TypeElement, annotation: AnnotationMirror): String {
- val annotationValue = AnnotationMirrors
- .getAnnotationValue(annotation, "tableName").value.toString()
- return if (annotationValue == "") {
- element.simpleName.toString()
- } else {
- annotationValue
- }
- }
-
- private fun extractIndices(
- annotation: AnnotationMirror, tableName: String): List<IndexInput> {
- val arrayOfIndexAnnotations = AnnotationMirrors.getAnnotationValue(annotation,
- "indices")
- return INDEX_LIST_VISITOR.visit(arrayOfIndexAnnotations, tableName)
- }
-
- private val INDEX_LIST_VISITOR = object
- : SimpleAnnotationValueVisitor6<List<IndexInput>, String>() {
- override fun visitArray(
- values: MutableList<out AnnotationValue>?,
- tableName: String
- ): List<IndexInput> {
- return values?.map {
- INDEX_VISITOR.visit(it, tableName)
- }?.filterNotNull() ?: emptyList<IndexInput>()
- }
- }
-
- private val INDEX_VISITOR = object : SimpleAnnotationValueVisitor6<IndexInput?, String>() {
- override fun visitAnnotation(a: AnnotationMirror?, tableName: String): IndexInput? {
- val fieldInput = getAnnotationValue(a, "value").getAsStringList()
- val unique = getAnnotationValue(a, "unique").getAsBoolean(false)
- val nameValue = getAnnotationValue(a, "name")
- .getAsString("")
- val name = if (nameValue == null || nameValue == "") {
- createIndexName(fieldInput, tableName)
- } else {
- nameValue
- }
- return IndexInput(name, unique, fieldInput)
- }
- }
-
- private fun createIndexName(columnNames: List<String>, tableName: String): String {
- return Index.DEFAULT_PREFIX + tableName + "_" + columnNames.joinToString("_")
- }
-
- private fun extractForeignKeys(annotation: AnnotationMirror): List<ForeignKeyInput> {
- val arrayOfForeignKeyAnnotations = getAnnotationValue(annotation, "foreignKeys")
- return FOREIGN_KEY_LIST_VISITOR.visit(arrayOfForeignKeyAnnotations)
- }
-
- private val FOREIGN_KEY_LIST_VISITOR = object
- : SimpleAnnotationValueVisitor6<List<ForeignKeyInput>, Void?>() {
- override fun visitArray(
- values: MutableList<out AnnotationValue>?,
- void: Void?
- ): List<ForeignKeyInput> {
- return values?.map {
- FOREIGN_KEY_VISITOR.visit(it)
- }?.filterNotNull() ?: emptyList<ForeignKeyInput>()
- }
- }
-
- private val FOREIGN_KEY_VISITOR = object : SimpleAnnotationValueVisitor6<ForeignKeyInput?,
- Void?>() {
- override fun visitAnnotation(a: AnnotationMirror?, void: Void?): ForeignKeyInput? {
- val entityClass = try {
- getAnnotationValue(a, "entity").toType()
- } catch (notPresent: TypeNotPresentException) {
- return null
- }
- val parentColumns = getAnnotationValue(a, "parentColumns").getAsStringList()
- val childColumns = getAnnotationValue(a, "childColumns").getAsStringList()
- val onDeleteInput = getAnnotationValue(a, "onDelete").getAsInt()
- val onUpdateInput = getAnnotationValue(a, "onUpdate").getAsInt()
- val deferred = getAnnotationValue(a, "deferred").getAsBoolean(true)
- val onDelete = ForeignKeyAction.fromAnnotationValue(onDeleteInput)
- val onUpdate = ForeignKeyAction.fromAnnotationValue(onUpdateInput)
- return ForeignKeyInput(
- parent = entityClass,
- parentColumns = parentColumns,
- childColumns = childColumns,
- onDelete = onDelete,
- onUpdate = onUpdate,
- deferred = deferred)
- }
- }
- }
-
- /**
- * processed Index annotation output
- */
- data class IndexInput(val name: String, val unique: Boolean, val columnNames: List<String>)
-
- /**
- * ForeignKey, before it is processed in the context of a database.
- */
- data class ForeignKeyInput(
- val parent: TypeMirror,
- val parentColumns: List<String>,
- val childColumns: List<String>,
- val onDelete: ForeignKeyAction?,
- val onUpdate: ForeignKeyAction?,
- val deferred: Boolean)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/FieldProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/FieldProcessor.kt
deleted file mode 100644
index f03d7da..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/FieldProcessor.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.ColumnInfo
-import android.arch.persistence.room.ext.getAsBoolean
-import android.arch.persistence.room.ext.getAsInt
-import android.arch.persistence.room.ext.getAsString
-import android.arch.persistence.room.parser.Collate
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.Field
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.Element
-import javax.lang.model.type.DeclaredType
-
-class FieldProcessor(baseContext: Context, val containing: DeclaredType, val element: Element,
- val bindingScope: BindingScope,
- // pass only if this is processed as a child of Embedded field
- val fieldParent: EmbeddedField?) {
- val context = baseContext.fork(element)
- fun process(): Field {
- val member = context.processingEnv.typeUtils.asMemberOf(containing, element)
- val type = TypeName.get(member)
- val columnInfoAnnotation = MoreElements.getAnnotationMirror(element,
- ColumnInfo::class.java)
- val name = element.simpleName.toString()
- val columnName: String
- val affinity: SQLTypeAffinity?
- val collate: Collate?
- val fieldPrefix = fieldParent?.prefix ?: ""
- val indexed: Boolean
- if (columnInfoAnnotation.isPresent) {
- val nameInAnnotation = AnnotationMirrors
- .getAnnotationValue(columnInfoAnnotation.get(), "name")
- .getAsString(ColumnInfo.INHERIT_FIELD_NAME)
- columnName = fieldPrefix + if (nameInAnnotation == ColumnInfo.INHERIT_FIELD_NAME) {
- name
- } else {
- nameInAnnotation
- }
-
- affinity = try {
- val userDefinedAffinity = AnnotationMirrors
- .getAnnotationValue(columnInfoAnnotation.get(), "typeAffinity")
- .getAsInt(ColumnInfo.UNDEFINED)!!
- SQLTypeAffinity.fromAnnotationValue(userDefinedAffinity)
- } catch (ex: NumberFormatException) {
- null
- }
-
- collate = Collate.fromAnnotationValue(AnnotationMirrors.getAnnotationValue(
- columnInfoAnnotation.get(), "collate").getAsInt(ColumnInfo.UNSPECIFIED)!!)
-
- indexed = AnnotationMirrors
- .getAnnotationValue(columnInfoAnnotation.get(), "index")
- .getAsBoolean(false)
- } else {
- columnName = fieldPrefix + name
- affinity = null
- collate = null
- indexed = false
- }
- context.checker.notBlank(columnName, element,
- ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY)
- context.checker.notUnbound(type, element,
- ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
-
- val field = Field(name = name,
- type = member,
- element = element,
- columnName = columnName,
- affinity = affinity,
- collate = collate,
- parent = fieldParent,
- indexed = indexed)
-
- when (bindingScope) {
- BindingScope.TWO_WAY -> {
- val adapter = context.typeAdapterStore.findColumnTypeAdapter(field.type,
- field.affinity)
- field.statementBinder = adapter
- field.cursorValueReader = adapter
- field.affinity = adapter?.typeAffinity ?: field.affinity
- context.checker.check(adapter != null, field.element,
- ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER)
- }
- BindingScope.BIND_TO_STMT -> {
- field.statementBinder = context.typeAdapterStore
- .findStatementValueBinder(field.type, field.affinity)
- context.checker.check(field.statementBinder != null, field.element,
- ProcessorErrors.CANNOT_FIND_STMT_BINDER)
- }
- BindingScope.READ_FROM_CURSOR -> {
- field.cursorValueReader = context.typeAdapterStore
- .findCursorValueReader(field.type, field.affinity)
- context.checker.check(field.cursorValueReader != null, field.element,
- ProcessorErrors.CANNOT_FIND_CURSOR_READER)
- }
- }
- return field
- }
-
- /**
- * Defines what we need to assign
- */
- enum class BindingScope {
- TWO_WAY, // both bind and read.
- BIND_TO_STMT, // just value to statement
- READ_FROM_CURSOR // just cursor to value
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/InsertionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/InsertionMethodProcessor.kt
deleted file mode 100644
index 02b191c..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/InsertionMethodProcessor.kt
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2016 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.
- */
-
-@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-
-package android.arch.persistence.room.processor
-
-import android.support.annotation.VisibleForTesting
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.OnConflictStrategy.IGNORE
-import android.arch.persistence.room.OnConflictStrategy.REPLACE
-import android.arch.persistence.room.vo.InsertionMethod
-import android.arch.persistence.room.vo.InsertionMethod.Type
-import android.arch.persistence.room.vo.ShortcutQueryParameter
-import com.google.auto.common.MoreTypes
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeKind.LONG
-import javax.lang.model.type.TypeKind.VOID
-import javax.lang.model.type.TypeMirror
-
-class InsertionMethodProcessor(baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement) {
- val context = baseContext.fork(executableElement)
- fun process(): InsertionMethod {
- val delegate = ShortcutMethodProcessor(context, containing, executableElement)
- val annotation = delegate.extractAnnotation(Insert::class,
- ProcessorErrors.MISSING_INSERT_ANNOTATION)
-
- val onConflict = OnConflictProcessor.extractFrom(annotation)
- context.checker.check(onConflict <= IGNORE && onConflict >= REPLACE,
- executableElement, ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
-
- val returnType = delegate.extractReturnType()
- val returnTypeName = TypeName.get(returnType)
- context.checker.notUnbound(returnTypeName, executableElement,
- ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_INSERTION_METHODS)
-
- val (entities, params) = delegate.extractParams(
- missingParamError = ProcessorErrors
- .INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT
- )
-
- // TODO we can support more types
- var insertionType = getInsertionType(returnType)
- context.checker.check(insertionType != null, executableElement,
- ProcessorErrors.INVALID_INSERTION_METHOD_RETURN_TYPE)
-
- if (insertionType != null) {
- val acceptable = acceptableTypes(params)
- if (insertionType !in acceptable) {
- context.logger.e(executableElement,
- ProcessorErrors.insertionMethodReturnTypeMismatch(
- insertionType.returnTypeName,
- acceptable.map { it.returnTypeName }))
- // clear it, no reason to generate code for it.
- insertionType = null
- }
- }
- return InsertionMethod(
- element = executableElement,
- name = executableElement.simpleName.toString(),
- returnType = returnType,
- entities = entities,
- parameters = params,
- onConflict = onConflict,
- insertionType = insertionType
- )
- }
-
- @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
- private fun getInsertionType(returnType: TypeMirror): InsertionMethod.Type? {
- // TODO we need to support more types here.
- fun isLongPrimitiveType(typeMirror: TypeMirror) = typeMirror.kind == LONG
-
- fun isLongBoxType(typeMirror: TypeMirror) =
- MoreTypes.isType(typeMirror) &&
- MoreTypes.isTypeOf(java.lang.Long::class.java, typeMirror)
-
- fun isLongType(typeMirror: TypeMirror) =
- isLongPrimitiveType(typeMirror) || isLongBoxType(typeMirror)
-
- return if (returnType.kind == VOID) {
- Type.INSERT_VOID
- } else if (returnType.kind == TypeKind.ARRAY) {
- val arrayType = MoreTypes.asArray(returnType)
- val param = arrayType.componentType
- if (isLongPrimitiveType(param)) {
- Type.INSERT_ID_ARRAY
- } else if (isLongBoxType(param)) {
- Type.INSERT_ID_ARRAY_BOX
- } else {
- null
- }
- } else if (MoreTypes.isType(returnType)
- && MoreTypes.isTypeOf(List::class.java, returnType)) {
- val declared = MoreTypes.asDeclared(returnType)
- val param = declared.typeArguments.first()
- if (isLongBoxType(param)) {
- Type.INSERT_ID_LIST
- } else {
- null
- }
- } else if (isLongType(returnType)) {
- Type.INSERT_SINGLE_ID
- } else {
- null
- }
- }
-
- companion object {
- @VisibleForTesting
- val VOID_SET by lazy { setOf(Type.INSERT_VOID) }
- @VisibleForTesting
- val SINGLE_ITEM_SET by lazy { setOf(Type.INSERT_VOID, Type.INSERT_SINGLE_ID) }
- @VisibleForTesting
- val MULTIPLE_ITEM_SET by lazy {
- setOf(Type.INSERT_VOID, Type.INSERT_ID_ARRAY, Type.INSERT_ID_ARRAY_BOX,
- Type.INSERT_ID_LIST)
- }
- fun acceptableTypes(params: List<ShortcutQueryParameter>): Set<InsertionMethod.Type> {
- if (params.isEmpty()) {
- return VOID_SET
- }
- if (params.size > 1) {
- return VOID_SET
- }
- if (params.first().isMultiple) {
- return MULTIPLE_ITEM_SET
- } else {
- return SINGLE_ITEM_SET
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/OnConflictProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/OnConflictProcessor.kt
deleted file mode 100644
index ac5ed5b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/OnConflictProcessor.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.OnConflictStrategy
-import com.google.auto.common.AnnotationMirrors
-import javax.lang.model.element.AnnotationMirror
-
-/**
- * Processes on conflict fields in annotations
- */
-object OnConflictProcessor {
- val INVALID_ON_CONFLICT = -1
-
- @OnConflictStrategy
- fun extractFrom(annotation: AnnotationMirror?, fieldName: String = "onConflict"): Int {
- return if (annotation == null) {
- INVALID_ON_CONFLICT
- } else {
- try {
- val onConflictValue = AnnotationMirrors
- .getAnnotationValue(annotation, fieldName)
- .value
- onConflictValue.toString().toInt()
- } catch (ex: NumberFormatException) {
- INVALID_ON_CONFLICT
- }
- }
- }
-
- fun onConflictText(@OnConflictStrategy onConflict: Int): String {
- return when (onConflict) {
- OnConflictStrategy.REPLACE -> "REPLACE"
- OnConflictStrategy.ABORT -> "ABORT"
- OnConflictStrategy.FAIL -> "FAIL"
- OnConflictStrategy.IGNORE -> "IGNORE"
- OnConflictStrategy.ROLLBACK -> "ROLLBACK"
- else -> "BAD_CONFLICT_CONSTRAINT"
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoMethodProcessor.kt
deleted file mode 100644
index e08b816..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoMethodProcessor.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2018 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.processor
-
-import android.arch.persistence.room.vo.PojoMethod
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-
-/**
- * processes an executable element as member of the owning class
- */
-class PojoMethodProcessor(
- private val context: Context,
- private val element: ExecutableElement,
- private val owner: DeclaredType) {
- fun process(): PojoMethod {
- val asMember = context.processingEnv.typeUtils.asMemberOf(owner, element)
- val name = element.simpleName.toString()
- return PojoMethod(
- element = element,
- resolvedType = MoreTypes.asExecutable(asMember),
- name = name
- )
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
deleted file mode 100644
index 6ed4034..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
+++ /dev/null
@@ -1,726 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.ColumnInfo
-import android.arch.persistence.room.Embedded
-import android.arch.persistence.room.Ignore
-import android.arch.persistence.room.Relation
-import android.arch.persistence.room.ext.KotlinMetadataProcessor
-import android.arch.persistence.room.ext.getAllFieldsIncludingPrivateSupers
-import android.arch.persistence.room.ext.getAnnotationValue
-import android.arch.persistence.room.ext.getAsString
-import android.arch.persistence.room.ext.getAsStringList
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.hasAnyOf
-import android.arch.persistence.room.ext.isAssignableWithoutVariance
-import android.arch.persistence.room.ext.isCollection
-import android.arch.persistence.room.ext.toClassType
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_TYPE
-import android.arch.persistence.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
-import android.arch.persistence.room.processor.cache.Cache
-import android.arch.persistence.room.vo.CallType
-import android.arch.persistence.room.vo.Constructor
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.FieldGetter
-import android.arch.persistence.room.vo.FieldSetter
-import android.arch.persistence.room.vo.Pojo
-import android.arch.persistence.room.vo.PojoMethod
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
-import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PROTECTED
-import javax.lang.model.element.Modifier.PUBLIC
-import javax.lang.model.element.Modifier.STATIC
-import javax.lang.model.element.Modifier.TRANSIENT
-import javax.lang.model.element.Name
-import javax.lang.model.element.TypeElement
-import javax.lang.model.element.VariableElement
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-import javax.lang.model.util.ElementFilter
-
-/**
- * Processes any class as if it is a Pojo.
- */
-class PojoProcessor(
- baseContext: Context,
- val element: TypeElement,
- val bindingScope: FieldProcessor.BindingScope,
- val parent: EmbeddedField?,
- val referenceStack: LinkedHashSet<Name> = LinkedHashSet())
- : KotlinMetadataProcessor {
- val context = baseContext.fork(element)
-
- // for KotlinMetadataUtils
- override val processingEnv: ProcessingEnvironment
- get() = context.processingEnv
-
- // opportunistic kotlin metadata
- private val kotlinMetadata by lazy {
- try {
- element.kotlinMetadata
- } catch (throwable: Throwable) {
- context.logger.d(element, "failed to read get kotlin metadata from %s", element)
- } as? KotlinClassMetadata
- }
-
- companion object {
- val PROCESSED_ANNOTATIONS = listOf(ColumnInfo::class, Embedded::class,
- Relation::class)
- }
-
- fun process(): Pojo {
- return context.cache.pojos.get(Cache.PojoKey(element, bindingScope, parent), {
- referenceStack.add(element.qualifiedName)
- try {
- doProcess()
- } finally {
- referenceStack.remove(element.qualifiedName)
- }
- })
- }
-
- private fun doProcess(): Pojo {
- val declaredType = MoreTypes.asDeclared(element.asType())
- // TODO handle conflicts with super: b/35568142
- val allFields = element.getAllFieldsIncludingPrivateSupers(context.processingEnv)
- .filter {
- !it.hasAnnotation(Ignore::class)
- && !it.hasAnyOf(STATIC)
- && (!it.hasAnyOf(TRANSIENT)
- || it.hasAnnotation(ColumnInfo::class)
- || it.hasAnnotation(Embedded::class)
- || it.hasAnnotation(Relation::class))
- }
- .groupBy { field ->
- context.checker.check(
- PROCESSED_ANNOTATIONS.count { field.hasAnnotation(it) } < 2, field,
- ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION
- )
- if (field.hasAnnotation(Embedded::class)) {
- Embedded::class
- } else if (field.hasAnnotation(Relation::class)) {
- Relation::class
- } else {
- null
- }
- }
-
- val myFields = allFields[null]
- ?.map {
- FieldProcessor(
- baseContext = context,
- containing = declaredType,
- element = it,
- bindingScope = bindingScope,
- fieldParent = parent).process()
- } ?: emptyList()
-
- val embeddedFields =
- allFields[Embedded::class]
- ?.map {
- processEmbeddedField(declaredType, it)
- }
- ?.filterNotNull()
- ?: emptyList()
-
- val subFields = embeddedFields.flatMap { it.pojo.fields }
- val fields = myFields + subFields
-
- val myRelationsList = allFields[Relation::class]
- ?.map {
- processRelationField(fields, declaredType, it)
- }
- ?.filterNotNull()
- ?: emptyList()
-
- val subRelations = embeddedFields.flatMap { it.pojo.relations }
- val relations = myRelationsList + subRelations
-
- fields.groupBy { it.columnName }
- .filter { it.value.size > 1 }
- .forEach {
- context.logger.e(element, ProcessorErrors.pojoDuplicateFieldNames(
- it.key, it.value.map(Field::getPath)
- ))
- it.value.forEach {
- context.logger.e(it.element, POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME)
- }
- }
-
- val methods = MoreElements.getLocalAndInheritedMethods(element,
- context.processingEnv.elementUtils)
- .filter {
- !it.hasAnyOf(PRIVATE, ABSTRACT, STATIC)
- && !it.hasAnnotation(Ignore::class)
- }
- .map { MoreElements.asExecutable(it) }
- .map {
- PojoMethodProcessor(
- context = context,
- element = it,
- owner = declaredType
- ).process()
- }
-
- val getterCandidates = methods.filter {
- it.element.parameters.size == 0 && it.resolvedType.returnType.kind != TypeKind.VOID
- }
-
- val setterCandidates = methods.filter {
- it.element.parameters.size == 1 && it.resolvedType.returnType.kind == TypeKind.VOID
- }
-
- // don't try to find a constructor for binding to statement.
- val constructor = if (bindingScope == FieldProcessor.BindingScope.BIND_TO_STMT) {
- // we don't need to construct this POJO.
- null
- } else {
- chooseConstructor(myFields, embeddedFields, relations)
- }
-
- assignGetters(myFields, getterCandidates)
- assignSetters(myFields, setterCandidates, constructor)
-
- embeddedFields.forEach {
- assignGetter(it.field, getterCandidates)
- assignSetter(it.field, setterCandidates, constructor)
- }
-
- myRelationsList.forEach {
- assignGetter(it.field, getterCandidates)
- assignSetter(it.field, setterCandidates, constructor)
- }
-
- return Pojo(element = element,
- type = declaredType,
- fields = fields,
- embeddedFields = embeddedFields,
- relations = relations,
- constructor = constructor)
- }
-
- /**
- * Retrieves the parameter names of a method. If the method is inherited from a dependency
- * module, the parameter name is not available (not in java spec). For kotlin, since parameter
- * names are part of the API, we can read them via the kotlin metadata annotation.
- * <p>
- * Since we are using an unofficial library to read the metadata, all access to that code
- * is safe guarded to avoid unexpected failures. In other words, it is a best effort but
- * better than not supporting these until JB provides a proper API.
- */
- private fun getParamNames(method: ExecutableElement): List<String> {
- val paramNames = method.parameters.map { it.simpleName.toString() }
- if (paramNames.isEmpty()) {
- return emptyList()
- }
- return kotlinMetadata?.getParameterNames(method) ?: paramNames
- }
-
- private fun chooseConstructor(
- myFields: List<Field>,
- embedded: List<EmbeddedField>,
- relations: List<android.arch.persistence.room.vo.Relation>): Constructor? {
- val constructors = ElementFilter.constructorsIn(element.enclosedElements)
- .filterNot { it.hasAnnotation(Ignore::class) || it.hasAnyOf(PRIVATE) }
- val fieldMap = myFields.associateBy { it.name }
- val embeddedMap = embedded.associateBy { it.field.name }
- val typeUtils = context.processingEnv.typeUtils
- // list of param names -> matched params pairs for each failed constructor
- val failedConstructors = arrayListOf<FailedConstructor>()
- // if developer puts a relation into a constructor, it is usually an error but if there
- // is another constructor that is good, we can ignore the error. b/72884434
- val relationsInConstructor = arrayListOf<VariableElement>()
- val goodConstructors = constructors.map { constructor ->
- val parameterNames = getParamNames(constructor)
- val params = constructor.parameters.mapIndexed param@ { index, param ->
- val paramName = parameterNames[index]
- val paramType = param.asType()
-
- val matches = fun(field: Field?): Boolean {
- return if (field == null) {
- false
- } else if (!field.nameWithVariations.contains(paramName)) {
- false
- } else {
- // see: b/69164099
- typeUtils.isAssignableWithoutVariance(paramType, field.type)
- }
- }
-
- val exactFieldMatch = fieldMap[paramName]
-
- if (matches(exactFieldMatch)) {
- return@param Constructor.FieldParam(exactFieldMatch!!)
- }
- val exactEmbeddedMatch = embeddedMap[paramName]
- if (matches(exactEmbeddedMatch?.field)) {
- return@param Constructor.EmbeddedParam(exactEmbeddedMatch!!)
- }
-
- val matchingFields = myFields.filter {
- matches(it)
- }
- val embeddedMatches = embedded.filter {
- matches(it.field)
- }
- if (matchingFields.isEmpty() && embeddedMatches.isEmpty()) {
- // if it didn't match a proper field, a common mistake is to have a relation
- // so check to see if it is a relation
- val matchedRelation = relations.any {
- it.field.nameWithVariations.contains(paramName)
- }
- if (matchedRelation) {
- relationsInConstructor.add(param)
- }
- null
- } else if (matchingFields.size + embeddedMatches.size == 1) {
- if (matchingFields.isNotEmpty()) {
- Constructor.FieldParam(matchingFields.first())
- } else {
- Constructor.EmbeddedParam(embeddedMatches.first())
- }
- } else {
- context.logger.e(param, ProcessorErrors.ambigiousConstructor(
- pojo = element.qualifiedName.toString(),
- paramName = paramName,
- matchingFields = matchingFields.map { it.getPath() }
- + embedded.map { it.field.getPath() }
- ))
- null
- }
- }
- if (params.any { it == null }) {
- failedConstructors.add(FailedConstructor(constructor, parameterNames, params))
- null
- } else {
- @Suppress("UNCHECKED_CAST")
- Constructor(constructor, params as List<Constructor.Param>)
- }
- }.filterNotNull()
- when {
- goodConstructors.isEmpty() -> {
- relationsInConstructor.forEach {
- context.logger.e(it,
- ProcessorErrors.RELATION_CANNOT_BE_CONSTRUCTOR_PARAMETER)
- }
- if (failedConstructors.isNotEmpty()) {
- val failureMsg = failedConstructors.joinToString("\n") { entry ->
- entry.log()
- }
- context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR +
- "\nTried the following constructors but they failed to match:" +
- "\n$failureMsg")
- }
- context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
- return null
- }
- goodConstructors.size > 1 -> {
- // if there is a no-arg constructor, pick it. Even though it is weird, easily happens
- // with kotlin data classes.
- val noArg = goodConstructors.firstOrNull { it.params.isEmpty() }
- if (noArg != null) {
- context.logger.w(Warning.DEFAULT_CONSTRUCTOR, element,
- ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS_CHOOSING_NO_ARG)
- return noArg
- }
- goodConstructors.forEach {
- context.logger.e(it.element, ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS)
- }
- return null
- }
- else -> return goodConstructors.first()
- }
- }
-
- private fun processEmbeddedField(
- declaredType: DeclaredType?, variableElement: VariableElement): EmbeddedField? {
-
- val asTypeElement = MoreTypes.asTypeElement(variableElement.asType())
-
- if (detectReferenceRecursion(asTypeElement)) {
- return null
- }
-
- val fieldPrefix = variableElement
- .getAnnotationValue(Embedded::class.java, "prefix")
- ?.toString()
- ?: ""
- val inheritedPrefix = parent?.prefix ?: ""
- val embeddedField = Field(
- variableElement,
- variableElement.simpleName.toString(),
- type = context
- .processingEnv
- .typeUtils
- .asMemberOf(declaredType, variableElement),
- affinity = null,
- parent = parent)
- val subParent = EmbeddedField(
- field = embeddedField,
- prefix = inheritedPrefix + fieldPrefix,
- parent = parent)
- subParent.pojo = PojoProcessor(
- baseContext = context.fork(variableElement),
- element = asTypeElement,
- bindingScope = bindingScope,
- parent = subParent,
- referenceStack = referenceStack).process()
- return subParent
- }
-
- private fun processRelationField(
- myFields: List<Field>, container: DeclaredType?,
- relationElement: VariableElement
- ): android.arch.persistence.room.vo.Relation? {
- val asTypeElement = MoreTypes.asTypeElement(
- MoreElements.asVariable(relationElement).asType())
-
- if (detectReferenceRecursion(asTypeElement)) {
- return null
- }
-
- val annotation = MoreElements.getAnnotationMirror(relationElement, Relation::class.java)
- .orNull()!!
- val parentColumnInput = AnnotationMirrors.getAnnotationValue(annotation, "parentColumn")
- .getAsString("") ?: ""
-
- val parentField = myFields.firstOrNull {
- it.columnName == parentColumnInput
- }
- if (parentField == null) {
- context.logger.e(relationElement,
- ProcessorErrors.relationCannotFindParentEntityField(
- entityName = element.qualifiedName.toString(),
- columnName = parentColumnInput,
- availableColumns = myFields.map { it.columnName }))
- return null
- }
- // parse it as an entity.
- val asMember = MoreTypes
- .asMemberOf(context.processingEnv.typeUtils, container, relationElement)
- if (asMember.kind == TypeKind.ERROR) {
- context.logger.e(ProcessorErrors.CANNOT_FIND_TYPE, element)
- return null
- }
- val declared = MoreTypes.asDeclared(asMember)
- if (!declared.isCollection()) {
- context.logger.e(relationElement, ProcessorErrors.RELATION_NOT_COLLECTION)
- return null
- }
- val typeArg = declared.typeArguments.first()
- if (typeArg.kind == TypeKind.ERROR) {
- context.logger.e(MoreTypes.asTypeElement(typeArg), CANNOT_FIND_TYPE)
- return null
- }
- val typeArgElement = MoreTypes.asTypeElement(typeArg)
- val entityClassInput = AnnotationMirrors
- .getAnnotationValue(annotation, "entity").toClassType()
-
- // do we need to decide on the entity?
- val inferEntity = (entityClassInput == null
- || MoreTypes.isTypeOf(Any::class.java, entityClassInput))
-
- val entity = if (inferEntity) {
- EntityProcessor(context, typeArgElement, referenceStack).process()
- } else {
- EntityProcessor(context, MoreTypes.asTypeElement(entityClassInput),
- referenceStack).process()
- }
-
- // now find the field in the entity.
- val entityColumnInput = AnnotationMirrors.getAnnotationValue(annotation, "entityColumn")
- .getAsString() ?: ""
- val entityField = entity.fields.firstOrNull {
- it.columnName == entityColumnInput
- }
-
- if (entityField == null) {
- context.logger.e(relationElement,
- ProcessorErrors.relationCannotFindEntityField(
- entityName = entity.typeName.toString(),
- columnName = entityColumnInput,
- availableColumns = entity.fields.map { it.columnName }))
- return null
- }
-
- val field = Field(
- element = relationElement,
- name = relationElement.simpleName.toString(),
- type = context.processingEnv.typeUtils.asMemberOf(container, relationElement),
- affinity = null,
- parent = parent)
-
- val projectionInput = AnnotationMirrors.getAnnotationValue(annotation, "projection")
- .getAsStringList()
- val projection = if (projectionInput.isEmpty()) {
- // we need to infer the projection from inputs.
- createRelationshipProjection(inferEntity, typeArg, entity, entityField, typeArgElement)
- } else {
- // make sure projection makes sense
- validateRelationshipProjection(projectionInput, entity, relationElement)
- projectionInput
- }
- // if types don't match, row adapter prints a warning
- return android.arch.persistence.room.vo.Relation(
- entity = entity,
- pojoType = typeArg,
- field = field,
- parentField = parentField,
- entityField = entityField,
- projection = projection
- )
- }
-
- private fun validateRelationshipProjection(
- projectionInput: List<String>,
- entity: Entity,
- relationElement: VariableElement) {
- val missingColumns = projectionInput.filterNot { columnName ->
- entity.fields.any { columnName == it.columnName }
- }
- if (missingColumns.isNotEmpty()) {
- context.logger.e(relationElement,
- ProcessorErrors.relationBadProject(entity.typeName.toString(),
- missingColumns, entity.fields.map { it.columnName }))
- }
- }
-
- /**
- * Create the projection column list based on the relationship args.
- *
- * if entity field in the annotation is not specified, it is the method return type
- * if it is specified in the annotation:
- * still check the method return type, if the same, use it
- * if not, check to see if we can find a column Adapter, if so use the childField
- * last resort, try to parse it as a pojo to infer it.
- */
- private fun createRelationshipProjection(
- inferEntity: Boolean,
- typeArg: TypeMirror,
- entity: Entity,
- entityField: Field,
- typeArgElement: TypeElement): List<String> {
- return if (inferEntity || typeArg.typeName() == entity.typeName) {
- entity.fields.map { it.columnName }
- } else {
- val columnAdapter = context.typeAdapterStore.findCursorValueReader(typeArg, null)
- if (columnAdapter != null) {
- // nice, there is a column adapter for this, assume single column response
- listOf(entityField.name)
- } else {
- // last resort, it needs to be a pojo
- val pojo = PojoProcessor(
- baseContext = context,
- element = typeArgElement,
- bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
- parent = parent,
- referenceStack = referenceStack).process()
- pojo.fields.map { it.columnName }
- }
- }
- }
-
- private fun detectReferenceRecursion(typeElement: TypeElement): Boolean {
- if (referenceStack.contains(typeElement.qualifiedName)) {
- context.logger.e(
- typeElement,
- ProcessorErrors
- .RECURSIVE_REFERENCE_DETECTED
- .format(computeReferenceRecursionString(typeElement)))
- return true
- }
- return false
- }
-
- private fun computeReferenceRecursionString(typeElement: TypeElement): String {
- val recursiveTailTypeName = typeElement.qualifiedName
-
- val referenceRecursionList = mutableListOf<Name>()
- with(referenceRecursionList) {
- add(recursiveTailTypeName)
- addAll(referenceStack.toList().takeLastWhile { it != recursiveTailTypeName })
- add(recursiveTailTypeName)
- }
-
- return referenceRecursionList.joinToString(" -> ")
- }
-
- private fun assignGetters(fields: List<Field>, getterCandidates: List<PojoMethod>) {
- fields.forEach { field ->
- assignGetter(field, getterCandidates)
- }
- }
-
- private fun assignGetter(field: Field, getterCandidates: List<PojoMethod>) {
- val success = chooseAssignment(field = field,
- candidates = getterCandidates,
- nameVariations = field.getterNameWithVariations,
- getType = { method ->
- method.resolvedType.returnType
- },
- assignFromField = {
- field.getter = FieldGetter(
- name = field.name,
- type = field.type,
- callType = CallType.FIELD)
- },
- assignFromMethod = { match ->
- field.getter = FieldGetter(
- name = match.name,
- type = match.resolvedType.returnType,
- callType = CallType.METHOD)
- },
- reportAmbiguity = { matching ->
- context.logger.e(field.element,
- ProcessorErrors.tooManyMatchingGetters(field, matching))
- })
- context.checker.check(success, field.element, CANNOT_FIND_GETTER_FOR_FIELD)
- }
-
- private fun assignSetters(
- fields: List<Field>,
- setterCandidates: List<PojoMethod>,
- constructor: Constructor?) {
- fields.forEach { field ->
- assignSetter(field, setterCandidates, constructor)
- }
- }
-
- private fun assignSetter(
- field: Field,
- setterCandidates: List<PojoMethod>,
- constructor: Constructor?) {
- if (constructor != null && constructor.hasField(field)) {
- field.setter = FieldSetter(field.name, field.type, CallType.CONSTRUCTOR)
- return
- }
- val success = chooseAssignment(field = field,
- candidates = setterCandidates,
- nameVariations = field.setterNameWithVariations,
- getType = { method ->
- method.resolvedType.parameterTypes.first()
- },
- assignFromField = {
- field.setter = FieldSetter(
- name = field.name,
- type = field.type,
- callType = CallType.FIELD)
- },
- assignFromMethod = { match ->
- val paramType = match.resolvedType.parameterTypes.first()
- field.setter = FieldSetter(
- name = match.name,
- type = paramType,
- callType = CallType.METHOD)
- },
- reportAmbiguity = { matching ->
- context.logger.e(field.element,
- ProcessorErrors.tooManyMatchingSetter(field, matching))
- })
- context.checker.check(success, field.element, CANNOT_FIND_SETTER_FOR_FIELD)
- }
-
- /**
- * Finds a setter/getter from available list of methods.
- * It returns true if assignment is successful, false otherwise.
- * At worst case, it sets to the field as if it is accessible so that the rest of the
- * compilation can continue.
- */
- private fun chooseAssignment(
- field: Field,
- candidates: List<PojoMethod>,
- nameVariations: List<String>,
- getType: (PojoMethod) -> TypeMirror,
- assignFromField: () -> Unit,
- assignFromMethod: (PojoMethod) -> Unit,
- reportAmbiguity: (List<String>) -> Unit
- ): Boolean {
- if (field.element.hasAnyOf(PUBLIC)) {
- assignFromField()
- return true
- }
- val types = context.processingEnv.typeUtils
-
- val matching = candidates
- .filter {
- // b/69164099
- types.isAssignableWithoutVariance(getType(it), field.type)
- && (field.nameWithVariations.contains(it.name)
- || nameVariations.contains(it.name))
- }
- .groupBy {
- if (it.element.hasAnyOf(PUBLIC)) PUBLIC else PROTECTED
- }
- if (matching.isEmpty()) {
- // we always assign to avoid NPEs in the rest of the compilation.
- assignFromField()
- // if field is not private, assume it works (if we are on the same package).
- // if not, compiler will tell, we didn't have any better alternative anyways.
- return !field.element.hasAnyOf(PRIVATE)
- }
- val match = verifyAndChooseOneFrom(matching[PUBLIC], reportAmbiguity)
- ?: verifyAndChooseOneFrom(matching[PROTECTED], reportAmbiguity)
- if (match == null) {
- assignFromField()
- return false
- } else {
- assignFromMethod(match)
- return true
- }
- }
-
- private fun verifyAndChooseOneFrom(
- candidates: List<PojoMethod>?,
- reportAmbiguity: (List<String>) -> Unit
- ): PojoMethod? {
- if (candidates == null) {
- return null
- }
- if (candidates.size > 1) {
- reportAmbiguity(candidates.map { it.name })
- }
- return candidates.first()
- }
-
- private data class FailedConstructor(
- val method: ExecutableElement,
- val params: List<String>,
- val matches: List<Constructor.Param?>
- ) {
- fun log(): String {
- val logPerParam = params.withIndex().joinToString(", ") {
- "param:${it.value} -> matched field:" + (matches[it.index]?.log() ?: "unmatched")
- }
- return "$method -> [$logPerParam]"
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
deleted file mode 100644
index 9c3fecb..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ /dev/null
@@ -1,516 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Delete
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.RawQuery
-import android.arch.persistence.room.Update
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.vo.CustomTypeConverter
-import android.arch.persistence.room.vo.Field
-import com.squareup.javapoet.TypeName
-
-object ProcessorErrors {
- private fun String.trim(): String {
- return this.trimIndent().replace("\n", " ")
- }
- val MISSING_QUERY_ANNOTATION = "Query methods must be annotated with ${Query::class.java}"
- val MISSING_INSERT_ANNOTATION = "Insertion methods must be annotated with ${Insert::class.java}"
- val MISSING_DELETE_ANNOTATION = "Deletion methods must be annotated with ${Delete::class.java}"
- val MISSING_UPDATE_ANNOTATION = "Update methods must be annotated with ${Update::class.java}"
- val MISSING_RAWQUERY_ANNOTATION = "RawQuery methods must be annotated with" +
- " ${RawQuery::class.java}"
- val INVALID_ON_CONFLICT_VALUE = "On conflict value must be one of @OnConflictStrategy values."
- val INVALID_INSERTION_METHOD_RETURN_TYPE = "Methods annotated with @Insert can return either" +
- " void, long, Long, long[], Long[] or List<Long>."
- val TRANSACTION_REFERENCE_DOCS = "https://developer.android.com/reference/android/arch/" +
- "persistence/room/Transaction.html"
-
- fun insertionMethodReturnTypeMismatch(definedReturn: TypeName,
- expectedReturnTypes: List<TypeName>): String {
- return "Method returns $definedReturn but it should return one of the following: `" +
- expectedReturnTypes.joinToString(", ") + "`. If you want to return the list of" +
- " row ids from the query, your insertion method can receive only 1 parameter."
- }
-
- val ABSTRACT_METHOD_IN_DAO_MISSING_ANY_ANNOTATION = "Abstract method in DAO must be annotated" +
- " with ${Query::class.java} AND ${Insert::class.java}"
- val INVALID_ANNOTATION_COUNT_IN_DAO_METHOD = "An abstract DAO method must be" +
- " annotated with one and only one of the following annotations: " +
- DaoProcessor.PROCESSED_ANNOTATIONS.joinToString(",") {
- it.java.simpleName
- }
- val CANNOT_RESOLVE_RETURN_TYPE = "Cannot resolve return type for %s"
- val CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS = "Cannot use unbound generics in query" +
- " methods. It must be bound to a type through base Dao class."
- val CANNOT_USE_UNBOUND_GENERICS_IN_INSERTION_METHODS = "Cannot use unbound generics in" +
- " insertion methods. It must be bound to a type through base Dao class."
- val CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS = "Cannot use unbound fields in entities."
- val CANNOT_USE_UNBOUND_GENERICS_IN_DAO_CLASSES = "Cannot use unbound generics in Dao classes." +
- " If you are trying to create a base DAO, create a normal class, extend it with type" +
- " params then mark the subclass with @Dao."
- val CANNOT_FIND_GETTER_FOR_FIELD = "Cannot find getter for field."
- val CANNOT_FIND_SETTER_FOR_FIELD = "Cannot find setter for field."
- val MISSING_PRIMARY_KEY = "An entity must have at least 1 field annotated with @PrimaryKey"
- val AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT = "If a primary key is annotated with" +
- " autoGenerate, its type must be int, Integer, long or Long."
- val AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS = "When @PrimaryKey annotation is used on a" +
- " field annotated with @Embedded, the embedded class should have only 1 field."
-
- fun multiplePrimaryKeyAnnotations(primaryKeys: List<String>): String {
- return """
- You cannot have multiple primary keys defined in an Entity. If you
- want to declare a composite primary key, you should use @Entity#primaryKeys and
- not use @PrimaryKey. Defined Primary Keys:
- ${primaryKeys.joinToString(", ")}""".trim()
- }
-
- fun primaryKeyColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
- return "$columnName referenced in the primary key does not exists in the Entity." +
- " Available column names:${allColumns.joinToString(", ")}"
- }
-
- val DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE = "Dao class must be an abstract class or" +
- " an interface"
- val DATABASE_MUST_BE_ANNOTATED_WITH_DATABASE = "Database must be annotated with @Database"
- val DAO_MUST_BE_ANNOTATED_WITH_DAO = "Dao class must be annotated with @Dao"
-
- fun daoMustHaveMatchingConstructor(daoName: String, dbName: String): String {
- return """
- $daoName needs to have either an empty constructor or a constructor that takes
- $dbName as its only parameter.
- """.trim()
- }
-
- val ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY = "Entity class must be annotated with @Entity"
- val DATABASE_ANNOTATION_MUST_HAVE_LIST_OF_ENTITIES = "@Database annotation must specify list" +
- " of entities"
- val COLUMN_NAME_CANNOT_BE_EMPTY = "Column name cannot be blank. If you don't want to set it" +
- ", just remove the @ColumnInfo annotation or use @ColumnInfo.INHERIT_FIELD_NAME."
-
- val ENTITY_TABLE_NAME_CANNOT_BE_EMPTY = "Entity table name cannot be blank. If you don't want" +
- " to set it, just remove the tableName property."
-
- val CANNOT_BIND_QUERY_PARAMETER_INTO_STMT = "Query method parameters should either be a" +
- " type that can be converted into a database column or a List / Array that contains" +
- " such type. You can consider adding a Type Adapter for this."
-
- val QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE = "Query/Insert method parameters cannot " +
- "start with underscore (_)."
-
- val CANNOT_FIND_QUERY_RESULT_ADAPTER = "Not sure how to convert a Cursor to this method's " +
- "return type"
-
- val INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT = "Method annotated with" +
- " @Insert but does not have any parameters to insert."
-
- val DELETION_MISSING_PARAMS = "Method annotated with" +
- " @Delete but does not have any parameters to delete."
-
- val UPDATE_MISSING_PARAMS = "Method annotated with" +
- " @Update but does not have any parameters to update."
-
- val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
- "private, final, or abstract. It can be abstract only if the method is also" +
- " annotated with @Query."
-
- val TRANSACTION_MISSING_ON_RELATION = "The return value includes a Pojo with a @Relation." +
- " It is usually desired to annotate this method with @Transaction to avoid" +
- " possibility of inconsistent results between the Pojo and its relations. See " +
- TRANSACTION_REFERENCE_DOCS + " for details."
-
- val CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER = "Type of the parameter must be a class " +
- "annotated with @Entity or a collection/array of it."
-
- val DB_MUST_EXTEND_ROOM_DB = "Classes annotated with @Database should extend " +
- RoomTypeNames.ROOM_DB
-
- val LIVE_DATA_QUERY_WITHOUT_SELECT = "LiveData return type can only be used with SELECT" +
- " queries."
-
- val OBSERVABLE_QUERY_NOTHING_TO_OBSERVE = "Observable query return type (LiveData, Flowable" +
- ", DataSource, DataSourceFactory etc) can only be used with SELECT queries that" +
- " directly or indirectly (via @Relation, for example) access at least one table. For" +
- " @RawQuery, you should specify the list of tables to be observed via the" +
- " observedEntities field."
-
- val RECURSIVE_REFERENCE_DETECTED = "Recursive referencing through @Embedded and/or @Relation " +
- "detected: %s"
-
- private val TOO_MANY_MATCHING_GETTERS = "Ambiguous getter for %s. All of the following " +
- "match: %s. You can @Ignore the ones that you don't want to match."
-
- fun tooManyMatchingGetters(field: Field, methodNames: List<String>): String {
- return TOO_MANY_MATCHING_GETTERS.format(field, methodNames.joinToString(", "))
- }
-
- private val TOO_MANY_MATCHING_SETTERS = "Ambiguous setter for %s. All of the following " +
- "match: %s. You can @Ignore the ones that you don't want to match."
-
- fun tooManyMatchingSetter(field: Field, methodNames: List<String>): String {
- return TOO_MANY_MATCHING_SETTERS.format(field, methodNames.joinToString(", "))
- }
-
- val CANNOT_FIND_COLUMN_TYPE_ADAPTER = "Cannot figure out how to save this field into" +
- " database. You can consider adding a type converter for it."
-
- val CANNOT_FIND_STMT_BINDER = "Cannot figure out how to bind this field into a statement."
-
- val CANNOT_FIND_CURSOR_READER = "Cannot figure out how to read this field from a cursor."
-
- private val MISSING_PARAMETER_FOR_BIND = "Each bind variable in the query must have a" +
- " matching method parameter. Cannot find method parameters for %s."
-
- fun missingParameterForBindVariable(bindVarName: List<String>): String {
- return MISSING_PARAMETER_FOR_BIND.format(bindVarName.joinToString(", "))
- }
-
- private val UNUSED_QUERY_METHOD_PARAMETER = "Unused parameter%s: %s"
- fun unusedQueryMethodParameter(unusedParams: List<String>): String {
- return UNUSED_QUERY_METHOD_PARAMETER.format(
- if (unusedParams.size > 1) "s" else "",
- unusedParams.joinToString(","))
- }
-
- private val DUPLICATE_TABLES = "Table name \"%s\" is used by multiple entities: %s"
- fun duplicateTableNames(tableName: String, entityNames: List<String>): String {
- return DUPLICATE_TABLES.format(tableName, entityNames.joinToString(", "))
- }
-
- val DELETION_METHODS_MUST_RETURN_VOID_OR_INT = "Deletion methods must either return void or" +
- " return int (the number of deleted rows)."
-
- val UPDATE_METHODS_MUST_RETURN_VOID_OR_INT = "Update methods must either return void or" +
- " return int (the number of updated rows)."
-
- val DAO_METHOD_CONFLICTS_WITH_OTHERS = "Dao method has conflicts."
-
- fun duplicateDao(dao: TypeName, methodNames: List<String>): String {
- return """
- All of these functions [${methodNames.joinToString(", ")}] return the same DAO
- class [$dao].
- A database can use a DAO only once so you should remove ${methodNames.size - 1} of
- these conflicting DAO methods. If you are implementing any of these to fulfill an
- interface, don't make it abstract, instead, implement the code that calls the
- other one.
- """.trim()
- }
-
- fun pojoMissingNonNull(pojoTypeName: TypeName, missingPojoFields: List<String>,
- allQueryColumns: List<String>): String {
- return """
- The columns returned by the query does not have the fields
- [${missingPojoFields.joinToString(",")}] in $pojoTypeName even though they are
- annotated as non-null or primitive.
- Columns returned by the query: [${allQueryColumns.joinToString(",")}]
- """.trim()
- }
-
- fun cursorPojoMismatch(pojoTypeName: TypeName,
- unusedColumns: List<String>, allColumns: List<String>,
- unusedFields: List<Field>, allFields: List<Field>): String {
- val unusedColumnsWarning = if (unusedColumns.isNotEmpty()) {
- """
- The query returns some columns [${unusedColumns.joinToString(", ")}] which are not
- use by $pojoTypeName. You can use @ColumnInfo annotation on the fields to specify
- the mapping.
- """.trim()
- } else {
- ""
- }
- val unusedFieldsWarning = if (unusedFields.isNotEmpty()) {
- """
- $pojoTypeName has some fields
- [${unusedFields.joinToString(", ") { it.columnName }}] which are not returned by the
- query. If they are not supposed to be read from the result, you can mark them with
- @Ignore annotation.
- """.trim()
- } else {
- ""
- }
- return """
- $unusedColumnsWarning
- $unusedFieldsWarning
- You can suppress this warning by annotating the method with
- @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH).
- Columns returned by the query: ${allColumns.joinToString(", ")}.
- Fields in $pojoTypeName: ${allFields.joinToString(", ") { it.columnName }}.
- """.trim()
- }
-
- val TYPE_CONVERTER_UNBOUND_GENERIC = "Cannot use unbound generics in Type Converters."
- val TYPE_CONVERTER_BAD_RETURN_TYPE = "Invalid return type for a type converter."
- val TYPE_CONVERTER_MUST_RECEIVE_1_PARAM = "Type converters must receive 1 parameter."
- val TYPE_CONVERTER_EMPTY_CLASS = "Class is referenced as a converter but it does not have any" +
- " converter methods."
- val TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR = "Classes that are used as TypeConverters must" +
- " have no-argument public constructors."
- val TYPE_CONVERTER_MUST_BE_PUBLIC = "Type converters must be public."
-
- fun duplicateTypeConverters(converters: List<CustomTypeConverter>): String {
- return "Multiple methods define the same conversion. Conflicts with these:" +
- " ${converters.joinToString(", ") { it.toString() }}"
- }
-
- // TODO must print field paths.
- val POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME = "Field has non-unique column name."
-
- fun pojoDuplicateFieldNames(columnName: String, fieldPaths: List<String>): String {
- return "Multiple fields have the same columnName: $columnName." +
- " Field names: ${fieldPaths.joinToString(", ")}."
- }
-
- fun embeddedPrimaryKeyIsDropped(entityQName: String, fieldName: String): String {
- return "Primary key constraint on $fieldName is ignored when being merged into " +
- entityQName
- }
-
- val INDEX_COLUMNS_CANNOT_BE_EMPTY = "List of columns in an index cannot be empty"
-
- fun indexColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
- return "$columnName referenced in the index does not exists in the Entity." +
- " Available column names:${allColumns.joinToString(", ")}"
- }
-
- fun duplicateIndexInEntity(indexName: String): String {
- return "There are multiple indices with name $indexName. This happen if you've declared" +
- " the same index multiple times or different indices have the same name. See" +
- " @Index documentation for details."
- }
-
- fun duplicateIndexInDatabase(indexName: String, indexPaths: List<String>): String {
- return "There are multiple indices with name $indexName. You should rename " +
- "${indexPaths.size - 1} of these to avoid the conflict:" +
- "${indexPaths.joinToString(", ")}."
- }
-
- fun droppedEmbeddedFieldIndex(fieldPath: String, grandParent: String): String {
- return "The index will be dropped when being merged into $grandParent" +
- "($fieldPath). You must re-declare it in $grandParent if you want to index this" +
- " field in $grandParent."
- }
-
- fun droppedEmbeddedIndex(entityName: String, fieldPath: String, grandParent: String): String {
- return "Indices defined in $entityName will be dropped when it is merged into" +
- " $grandParent ($fieldPath). You can re-declare them in $grandParent."
- }
-
- fun droppedSuperClassIndex(childEntity: String, superEntity: String): String {
- return "Indices defined in $superEntity will NOT be re-used in $childEntity. If you want" +
- " to inherit them, you must re-declare them in $childEntity." +
- " Alternatively, you can set inheritSuperIndices to true in the @Entity annotation."
- }
-
- fun droppedSuperClassFieldIndex(fieldName: String, childEntity: String,
- superEntity: String): String {
- return "Index defined on field `$fieldName` in $superEntity will NOT be re-used in" +
- " $childEntity. " +
- "If you want to inherit it, you must re-declare it in $childEntity." +
- " Alternatively, you can set inheritSuperIndices to true in the @Entity annotation."
- }
-
- val RELATION_NOT_COLLECTION = "Fields annotated with @Relation must be a List or Set."
-
- fun relationCannotFindEntityField(entityName: String, columnName: String,
- availableColumns: List<String>): String {
- return "Cannot find the child entity column `$columnName` in $entityName." +
- " Options: ${availableColumns.joinToString(", ")}"
- }
-
- fun relationCannotFindParentEntityField(entityName: String, columnName: String,
- availableColumns: List<String>): String {
- return "Cannot find the parent entity column `$columnName` in $entityName." +
- " Options: ${availableColumns.joinToString(", ")}"
- }
-
- val RELATION_IN_ENTITY = "Entities cannot have relations."
-
- val CANNOT_FIND_TYPE = "Cannot find type."
-
- fun relationAffinityMismatch(parentColumn: String, childColumn: String,
- parentAffinity: SQLTypeAffinity?,
- childAffinity: SQLTypeAffinity?): String {
- return """
- The affinity of parent column ($parentColumn : $parentAffinity) does not match the type
- affinity of the child column ($childColumn : $childAffinity).
- """.trim()
- }
-
- val CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION = "A field can be annotated with only" +
- " one of the following:" + PojoProcessor.PROCESSED_ANNOTATIONS.joinToString(",") {
- it.java.simpleName
- }
-
- fun relationBadProject(entityQName: String, missingColumnNames: List<String>,
- availableColumnNames: List<String>): String {
- return """
- $entityQName does not have the following columns: ${missingColumnNames.joinToString(",")}.
- Available columns are: ${availableColumnNames.joinToString(",")}
- """.trim()
- }
-
- val MISSING_SCHEMA_EXPORT_DIRECTORY = "Schema export directory is not provided to the" +
- " annotation processor so we cannot export the schema. You can either provide" +
- " `room.schemaLocation` annotation processor argument OR set exportSchema to false."
-
- val INVALID_FOREIGN_KEY_ACTION = "Invalid foreign key action. It must be one of the constants" +
- " defined in ForeignKey.Action"
-
- fun foreignKeyNotAnEntity(className: String): String {
- return """
- Classes referenced in Foreign Key annotations must be @Entity classes. $className is not
- an entity
- """.trim()
- }
-
- val FOREIGN_KEY_CANNOT_FIND_PARENT = "Cannot find parent entity class."
-
- fun foreignKeyChildColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
- return "($columnName) referenced in the foreign key does not exists in the Entity." +
- " Available column names:${allColumns.joinToString(", ")}"
- }
-
- fun foreignKeyParentColumnDoesNotExist(parentEntity: String,
- missingColumn: String,
- allColumns: List<String>): String {
- return "($missingColumn) does not exist in $parentEntity. Available columns are" +
- " ${allColumns.joinToString(",")}"
- }
-
- val FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST = "Must specify at least 1 column name for the child"
-
- val FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST = "Must specify at least 1 column name for the parent"
-
- fun foreignKeyColumnNumberMismatch(
- childColumns: List<String>, parentColumns: List<String>): String {
- return """
- Number of child columns in foreign key must match number of parent columns.
- Child reference has ${childColumns.joinToString(",")} and parent reference has
- ${parentColumns.joinToString(",")}
- """.trim()
- }
-
- fun foreignKeyMissingParentEntityInDatabase(parentTable: String, childEntity: String): String {
- return """
- $parentTable table referenced in the foreign keys of $childEntity does not exist in
- the database. Maybe you forgot to add the referenced entity in the entities list of
- the @Database annotation?""".trim()
- }
-
- fun foreignKeyMissingIndexInParent(parentEntity: String, parentColumns: List<String>,
- childEntity: String, childColumns: List<String>): String {
- return """
- $childEntity has a foreign key (${childColumns.joinToString(",")}) that references
- $parentEntity (${parentColumns.joinToString(",")}) but $parentEntity does not have
- a unique index on those columns nor the columns are its primary key.
- SQLite requires having a unique constraint on referenced parent columns so you must
- add a unique index to $parentEntity that has
- (${parentColumns.joinToString(",")}) column(s).
- """.trim()
- }
-
- fun foreignKeyMissingIndexInChildColumns(childColumns: List<String>): String {
- return """
- (${childColumns.joinToString(",")}) column(s) reference a foreign key but
- they are not part of an index. This may trigger full table scans whenever parent
- table is modified so you are highly advised to create an index that covers these
- columns.
- """.trim()
- }
-
- fun foreignKeyMissingIndexInChildColumn(childColumn: String): String {
- return """
- $childColumn column references a foreign key but it is not part of an index. This
- may trigger full table scans whenever parent table is modified so you are highly
- advised to create an index that covers this column.
- """.trim()
- }
-
- fun shortcutEntityIsNotInDatabase(database: String, dao: String, entity: String): String {
- return """
- $dao is part of $database but this entity is not in the database. Maybe you forgot
- to add $entity to the entities section of the @Database?
- """.trim()
- }
-
- val MISSING_ROOM_GUAVA_ARTIFACT = "To use Guava features, you must add `guava`" +
- " artifact from Room as a dependency. android.arch.persistence.room:guava:<version>"
-
- val MISSING_ROOM_RXJAVA2_ARTIFACT = "To use RxJava2 features, you must add `rxjava2`" +
- " artifact from Room as a dependency. android.arch.persistence.room:rxjava2:<version>"
-
- fun ambigiousConstructor(
- pojo: String, paramName: String, matchingFields: List<String>): String {
- return """
- Ambiguous constructor. The parameter ($paramName) in $pojo matches multiple fields:
- [${matchingFields.joinToString(",")}]. If you don't want to use this constructor,
- you can annotate it with @Ignore. If you want Room to use this constructor, you can
- rename the parameters to exactly match the field name to fix the ambiguity.
- """.trim()
- }
-
- val MISSING_POJO_CONSTRUCTOR = """
- Entities and Pojos must have a usable public constructor. You can have an empty
- constructor or a constructor whose parameters match the fields (by name and type).
- """.trim()
-
- val TOO_MANY_POJO_CONSTRUCTORS = """
- Room cannot pick a constructor since multiple constructors are suitable. Try to annotate
- unwanted constructors with @Ignore.
- """.trim()
-
- val TOO_MANY_POJO_CONSTRUCTORS_CHOOSING_NO_ARG = """
- There are multiple good constructors and Room will pick the no-arg constructor.
- You can use the @Ignore annotation to eliminate unwanted constructors.
- """.trim()
-
- val RELATION_CANNOT_BE_CONSTRUCTOR_PARAMETER = """
- Fields annotated with @Relation cannot be constructor parameters. These values are
- fetched after the object is constructed.
- """.trim()
-
- val PAGING_SPECIFY_DATA_SOURCE_TYPE = "For now, Room only supports PositionalDataSource class."
-
- fun primaryKeyNull(field: String): String {
- return "You must annotate primary keys with @NonNull. \"$field\" is nullable. SQLite " +
- "considers this a " +
- "bug and Room does not allow it. See SQLite docs for details: " +
- "https://www.sqlite.org/lang_createtable.html"
- }
-
- val INVALID_COLUMN_NAME = "Invalid column name. Room does not allow using ` or \" in column" +
- " names"
-
- val INVALID_TABLE_NAME = "Invalid table name. Room does not allow using ` or \" in table names"
-
- val RAW_QUERY_BAD_PARAMS = "RawQuery methods should have 1 and only 1 parameter with type" +
- " String or SupportSQLiteQuery"
-
- val RAW_QUERY_BAD_RETURN_TYPE = "RawQuery methods must return a non-void type."
-
- fun rawQueryBadEntity(typeName: TypeName): String {
- return """
- observedEntities field in RawQuery must either reference a class that is annotated
- with @Entity or it should reference a Pojo that either contains @Embedded fields that
- are annotated with @Entity or @Relation fields.
- $typeName does not have these properties, did you mean another class?
- """.trim()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
deleted file mode 100644
index e0728d7..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.SkipQueryVerification
-import android.arch.persistence.room.Transaction
-import android.arch.persistence.room.ext.KotlinMetadataProcessor
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.parser.QueryType
-import android.arch.persistence.room.parser.SqlParser
-import android.arch.persistence.room.solver.query.result.LiveDataQueryResultBinder
-import android.arch.persistence.room.solver.query.result.PojoRowAdapter
-import android.arch.persistence.room.verifier.DatabaseVerificaitonErrors
-import android.arch.persistence.room.verifier.DatabaseVerifier
-import android.arch.persistence.room.vo.QueryMethod
-import android.arch.persistence.room.vo.QueryParameter
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.squareup.javapoet.TypeName
-import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
-import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeKind
-
-class QueryMethodProcessor(
- baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement,
- val dbVerifier: DatabaseVerifier? = null
-) : KotlinMetadataProcessor {
- val context = baseContext.fork(executableElement)
-
- // for kotlin metadata
- override val processingEnv: ProcessingEnvironment
- get() = context.processingEnv
-
- private val classMetadata =
- try {
- containing.asElement().kotlinMetadata
- } catch (throwable: Throwable) {
- context.logger.d(executableElement,
- "failed to read get kotlin metadata from %s", executableElement)
- } as? KotlinClassMetadata
-
- fun process(): QueryMethod {
- val asMember = context.processingEnv.typeUtils.asMemberOf(containing, executableElement)
- val executableType = MoreTypes.asExecutable(asMember)
-
- val annotation = MoreElements.getAnnotationMirror(executableElement,
- Query::class.java).orNull()
- context.checker.check(annotation != null, executableElement,
- ProcessorErrors.MISSING_QUERY_ANNOTATION)
-
- val query = if (annotation != null) {
- val query = SqlParser.parse(
- AnnotationMirrors.getAnnotationValue(annotation, "value").value.toString())
- context.checker.check(query.errors.isEmpty(), executableElement,
- query.errors.joinToString("\n"))
- if (!executableElement.hasAnnotation(SkipQueryVerification::class)) {
- query.resultInfo = dbVerifier?.analyze(query.original)
- }
- if (query.resultInfo?.error != null) {
- context.logger.e(executableElement,
- DatabaseVerificaitonErrors.cannotVerifyQuery(query.resultInfo!!.error!!))
- }
-
- context.checker.check(executableType.returnType.kind != TypeKind.ERROR,
- executableElement, ProcessorErrors.CANNOT_RESOLVE_RETURN_TYPE,
- executableElement)
- query
- } else {
- ParsedQuery.MISSING
- }
-
- val returnTypeName = TypeName.get(executableType.returnType)
- context.checker.notUnbound(returnTypeName, executableElement,
- ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
-
- if (query.type == QueryType.DELETE) {
- context.checker.check(
- returnTypeName == TypeName.VOID || returnTypeName == TypeName.INT,
- executableElement,
- ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
- )
- }
- val resultBinder = context.typeAdapterStore
- .findQueryResultBinder(executableType.returnType, query)
- context.checker.check(resultBinder.adapter != null || query.type != QueryType.SELECT,
- executableElement, ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
- if (resultBinder is LiveDataQueryResultBinder) {
- context.checker.check(query.type == QueryType.SELECT, executableElement,
- ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
- }
-
- val inTransaction = when (query.type) {
- QueryType.SELECT -> executableElement.hasAnnotation(Transaction::class)
- else -> true
- }
-
- if (query.type == QueryType.SELECT && !inTransaction) {
- // put a warning if it is has relations and not annotated w/ transaction
- resultBinder.adapter?.rowAdapter?.let { rowAdapter ->
- if (rowAdapter is PojoRowAdapter
- && rowAdapter.relationCollectors.isNotEmpty()) {
- context.logger.w(Warning.RELATION_QUERY_WITHOUT_TRANSACTION,
- executableElement, ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
- }
- }
- }
- val kotlinParameterNames = classMetadata?.getParameterNames(executableElement)
-
- val parameters = executableElement.parameters
- .mapIndexed { index, variableElement ->
- QueryParameterProcessor(
- baseContext = context,
- containing = containing,
- element = variableElement,
- sqlName = kotlinParameterNames?.getOrNull(index)).process()
- }
- val queryMethod = QueryMethod(
- element = executableElement,
- query = query,
- name = executableElement.simpleName.toString(),
- returnType = executableType.returnType,
- parameters = parameters,
- inTransaction = inTransaction,
- queryResultBinder = resultBinder)
-
- val missing = queryMethod.sectionToParamMapping
- .filter { it.second == null }
- .map { it.first.text }
- if (missing.isNotEmpty()) {
- context.logger.e(executableElement,
- ProcessorErrors.missingParameterForBindVariable(missing))
- }
-
- val unused = queryMethod.parameters.filterNot { param ->
- queryMethod.sectionToParamMapping.any { it.second == param }
- }.map(QueryParameter::sqlName)
-
- if (unused.isNotEmpty()) {
- context.logger.e(executableElement, ProcessorErrors.unusedQueryMethodParameter(unused))
- }
- return queryMethod
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryParameterProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryParameterProcessor.kt
deleted file mode 100644
index 0a9016f..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryParameterProcessor.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.vo.QueryParameter
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.VariableElement
-import javax.lang.model.type.DeclaredType
-
-class QueryParameterProcessor(
- baseContext: Context,
- val containing: DeclaredType,
- val element: VariableElement,
- private val sqlName: String? = null) {
- val context = baseContext.fork(element)
- fun process(): QueryParameter {
- val asMember = MoreTypes.asMemberOf(context.processingEnv.typeUtils, containing, element)
- val parameterAdapter = context.typeAdapterStore.findQueryParameterAdapter(asMember)
- context.checker.check(parameterAdapter != null, element,
- ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
-
- val name = element.simpleName.toString()
- context.checker.check(!name.startsWith("_"), element,
- ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
- return QueryParameter(
- name = name,
- sqlName = sqlName ?: name,
- type = asMember,
- queryParamAdapter = parameterAdapter)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessor.kt
deleted file mode 100644
index b26ff6e..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessor.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2018 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.processor
-
-import android.arch.persistence.room.RawQuery
-import android.arch.persistence.room.Transaction
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.toListOfClassTypes
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.SqlParser
-import android.arch.persistence.room.vo.RawQueryMethod
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-
-class RawQueryMethodProcessor(
- baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement) {
- val context = baseContext.fork(executableElement)
- fun process(): RawQueryMethod {
- val types = context.processingEnv.typeUtils
- val asMember = types.asMemberOf(containing, executableElement)
- val executableType = MoreTypes.asExecutable(asMember)
-
- val annotation = MoreElements.getAnnotationMirror(executableElement,
- RawQuery::class.java).orNull()
- context.checker.check(annotation != null, executableElement,
- ProcessorErrors.MISSING_RAWQUERY_ANNOTATION)
-
- val returnTypeName = TypeName.get(executableType.returnType)
- context.checker.notUnbound(returnTypeName, executableElement,
- ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
- val observedTableNames = processObservedTables()
- val query = SqlParser.rawQueryForTables(observedTableNames)
- // build the query but don't calculate result info since we just guessed it.
- val resultBinder = context.typeAdapterStore
- .findQueryResultBinder(executableType.returnType, query)
-
- val runtimeQueryParam = findRuntimeQueryParameter()
- val inTransaction = executableElement.hasAnnotation(Transaction::class)
- val rawQueryMethod = RawQueryMethod(
- element = executableElement,
- name = executableElement.simpleName.toString(),
- observedTableNames = observedTableNames,
- returnType = executableType.returnType,
- runtimeQueryParam = runtimeQueryParam,
- inTransaction = inTransaction,
- queryResultBinder = resultBinder
- )
- context.checker.check(rawQueryMethod.returnsValue, executableElement,
- ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE)
- return rawQueryMethod
- }
-
- private fun processObservedTables(): Set<String> {
- val annotation = MoreElements
- .getAnnotationMirror(executableElement,
- android.arch.persistence.room.RawQuery::class.java)
- .orNull() ?: return emptySet()
- val entityList = AnnotationMirrors.getAnnotationValue(annotation, "observedEntities")
- return entityList
- .toListOfClassTypes()
- .map {
- MoreTypes.asTypeElement(it)
- }
- .flatMap {
- if (it.hasAnnotation(android.arch.persistence.room.Entity::class)) {
- val entity = EntityProcessor(
- baseContext = context,
- element = it
- ).process()
- arrayListOf(entity.tableName)
- } else {
- val pojo = PojoProcessor(
- baseContext = context,
- element = it,
- bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
- parent = null
- ).process()
- val tableNames = pojo.accessedTableNames()
- // if it is empty, report error as it does not make sense
- if (tableNames.isEmpty()) {
- context.logger.e(executableElement,
- ProcessorErrors.rawQueryBadEntity(it.asType().typeName()))
- }
- tableNames
- }
- }.toSet()
- }
-
- private fun findRuntimeQueryParameter(): RawQueryMethod.RuntimeQueryParameter? {
- val types = context.processingEnv.typeUtils
- if (executableElement.parameters.size == 1 && !executableElement.isVarArgs) {
- val param = MoreTypes.asMemberOf(
- types,
- containing,
- executableElement.parameters[0])
- val elementUtils = context.processingEnv.elementUtils
- val supportQueryType = elementUtils
- .getTypeElement(SupportDbTypeNames.QUERY.toString()).asType()
- val isSupportSql = types.isAssignable(param, supportQueryType)
- if (isSupportSql) {
- return RawQueryMethod.RuntimeQueryParameter(
- paramName = executableElement.parameters[0].simpleName.toString(),
- type = supportQueryType.typeName())
- }
- val stringType = elementUtils.getTypeElement("java.lang.String").asType()
- val isString = types.isAssignable(param, stringType)
- if (isString) {
- return RawQueryMethod.RuntimeQueryParameter(
- paramName = executableElement.parameters[0].simpleName.toString(),
- type = stringType.typeName())
- }
- }
- context.logger.e(executableElement, ProcessorErrors.RAW_QUERY_BAD_PARAMS)
- return null
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutMethodProcessor.kt
deleted file mode 100644
index c13b818..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutMethodProcessor.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.ShortcutQueryParameter
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.AnnotationMirror
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-import kotlin.reflect.KClass
-
-/**
- * Common functionality for shortcut method processors
- */
-class ShortcutMethodProcessor(baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement) {
- val context = baseContext.fork(executableElement)
- private val asMember = context.processingEnv.typeUtils.asMemberOf(containing, executableElement)
- private val executableType = MoreTypes.asExecutable(asMember)
-
- fun extractAnnotation(klass: KClass<out Annotation>,
- errorMsg: String): AnnotationMirror? {
- val annotation = MoreElements.getAnnotationMirror(executableElement,
- klass.java).orNull()
- context.checker.check(annotation != null, executableElement, errorMsg)
- return annotation
- }
-
- fun extractReturnType(): TypeMirror {
- return executableType.returnType
- }
-
- fun extractParams(
- missingParamError: String
- ): Pair<Map<String, Entity>, List<ShortcutQueryParameter>> {
- val params = executableElement.parameters
- .map { ShortcutParameterProcessor(
- baseContext = context,
- containing = containing,
- element = it).process() }
- context.checker.check(params.isNotEmpty(), executableElement, missingParamError)
- val entities = params
- .filter { it.entityType != null }
- .associateBy({ it.name }, {
- EntityProcessor(
- baseContext = context,
- element = MoreTypes.asTypeElement(it.entityType)).process()
- })
- return Pair(entities, params)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt
deleted file mode 100644
index 7fd6859..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ShortcutParameterProcessor.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.ext.extendsBound
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.vo.ShortcutQueryParameter
-import com.google.auto.common.MoreTypes
-import javax.lang.model.element.TypeElement
-import javax.lang.model.element.VariableElement
-import javax.lang.model.type.ArrayType
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-import javax.lang.model.util.ElementFilter
-
-/**
- * Processes parameters of methods that are annotated with Insert, Delete.
- */
-class ShortcutParameterProcessor(baseContext: Context,
- val containing: DeclaredType,
- val element: VariableElement) {
- val context = baseContext.fork(element)
- fun process(): ShortcutQueryParameter {
- val asMember = MoreTypes.asMemberOf(context.processingEnv.typeUtils, containing, element)
- val name = element.simpleName.toString()
- context.checker.check(!name.startsWith("_"), element,
- ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
-
- val (entityType, isMultiple) = extractEntityType(asMember)
- context.checker.check(entityType != null, element,
- ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER)
-
- return ShortcutQueryParameter(
- name = name,
- type = asMember,
- entityType = entityType,
- isMultiple = isMultiple
- )
- }
-
- @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
- fun extractEntityType(typeMirror: TypeMirror): Pair<TypeMirror?, Boolean> {
-
- val elementUtils = context.processingEnv.elementUtils
- val typeUtils = context.processingEnv.typeUtils
-
- fun verifyAndPair(entityType: TypeMirror, isMultiple: Boolean): Pair<TypeMirror?, Boolean> {
- if (!MoreTypes.isType(entityType)) {
- // kotlin may generate ? extends T so we should reduce it.
- val boundedVar = entityType.extendsBound()
- return boundedVar?.let {
- verifyAndPair(boundedVar, isMultiple)
- } ?: Pair(null, isMultiple)
- }
- val entityElement = MoreTypes.asElement(entityType)
- return if (entityElement.hasAnnotation(Entity::class)) {
- Pair(entityType, isMultiple)
- } else {
- Pair(null, isMultiple)
- }
- }
-
- fun extractEntityTypeFromIterator(iterableType: DeclaredType): TypeMirror {
- ElementFilter.methodsIn(elementUtils
- .getAllMembers(typeUtils.asElement(iterableType) as TypeElement)).forEach {
- if (it.simpleName.toString() == "iterator") {
- return MoreTypes.asDeclared(MoreTypes.asExecutable(
- typeUtils.asMemberOf(iterableType, it)).returnType)
- .typeArguments.first()
- }
- }
- throw IllegalArgumentException("iterator() not found in Iterable $iterableType")
- }
-
- val iterableType = typeUtils.erasure(elementUtils
- .getTypeElement("java.lang.Iterable").asType())
- if (typeUtils.isAssignable(typeMirror, iterableType)) {
- val declared = MoreTypes.asDeclared(typeMirror)
- val entity = extractEntityTypeFromIterator(declared)
- return verifyAndPair(entity, true)
- }
- if (typeMirror is ArrayType) {
- val entity = typeMirror.componentType
- return verifyAndPair(entity, true)
- }
- return verifyAndPair(typeMirror, false)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/SuppressWarningProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/SuppressWarningProcessor.kt
deleted file mode 100644
index 9edf2f9..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/SuppressWarningProcessor.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.AnnotationMirrors
-import com.google.auto.common.MoreElements
-import javax.lang.model.element.AnnotationValue
-import javax.lang.model.element.Element
-import javax.lang.model.util.SimpleAnnotationValueVisitor6
-
-/**
- * A visitor that reads SuppressWarnings annotations and keeps the ones we know about.
- */
-object SuppressWarningProcessor {
-
- fun getSuppressedWarnings(element: Element): Set<Warning> {
- val annotation = MoreElements.getAnnotationMirror(element,
- SuppressWarnings::class.java).orNull()
- return if (annotation == null) {
- emptySet<Warning>()
- } else {
- val value = AnnotationMirrors.getAnnotationValue(annotation, "value")
- if (value == null) {
- emptySet<Warning>()
- } else {
- VISITOR.visit(value)
- }
- }
- }
-
- private object VISITOR : SimpleAnnotationValueVisitor6<Set<Warning>, String>() {
- override fun visitArray(values: List<AnnotationValue>?, elementName: String?
- ): Set<Warning> {
- return values?.map {
- Warning.fromPublicKey(it.value.toString())
- }?.filterNotNull()?.toSet() ?: emptySet()
- }
-
- override fun defaultAction(o: Any?, p: String?): Set<Warning> {
- return emptySet()
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
deleted file mode 100644
index d35509d..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.ext.findKotlinDefaultImpl
-import android.arch.persistence.room.ext.hasAnyOf
-import android.arch.persistence.room.vo.TransactionMethod
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.element.Modifier.DEFAULT
-import javax.lang.model.element.Modifier.FINAL
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.type.DeclaredType
-
-class TransactionMethodProcessor(baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement) {
-
- val context = baseContext.fork(executableElement)
-
- fun process(): TransactionMethod {
- val kotlinDefaultImpl =
- executableElement.findKotlinDefaultImpl(context.processingEnv.typeUtils)
- context.checker.check(
- !executableElement.hasAnyOf(PRIVATE, FINAL)
- && (!executableElement.hasAnyOf(ABSTRACT) || kotlinDefaultImpl != null),
- executableElement, ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
-
- val callType = when {
- executableElement.hasAnyOf(DEFAULT) ->
- TransactionMethod.CallType.DEFAULT_JAVA8
- kotlinDefaultImpl != null ->
- TransactionMethod.CallType.DEFAULT_KOTLIN
- else ->
- TransactionMethod.CallType.CONCRETE
- }
-
- return TransactionMethod(
- element = executableElement,
- name = executableElement.simpleName.toString(),
- callType = callType)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/UpdateMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/UpdateMethodProcessor.kt
deleted file mode 100644
index 27211ce..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/UpdateMethodProcessor.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.OnConflictStrategy.IGNORE
-import android.arch.persistence.room.OnConflictStrategy.REPLACE
-import android.arch.persistence.room.Update
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.vo.UpdateMethod
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-
-class UpdateMethodProcessor(
- baseContext: Context,
- val containing: DeclaredType,
- val executableElement: ExecutableElement) {
- val context = baseContext.fork(executableElement)
-
- fun process(): UpdateMethod {
- val delegate = ShortcutMethodProcessor(context, containing, executableElement)
- val annotation = delegate
- .extractAnnotation(Update::class, ProcessorErrors.MISSING_UPDATE_ANNOTATION)
-
- val onConflict = OnConflictProcessor.extractFrom(annotation)
- context.checker.check(onConflict <= IGNORE && onConflict >= REPLACE,
- executableElement, ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
-
- val returnTypeName = delegate.extractReturnType().typeName()
- context.checker.check(
- returnTypeName == TypeName.VOID || returnTypeName == TypeName.INT,
- executableElement,
- ProcessorErrors.UPDATE_METHODS_MUST_RETURN_VOID_OR_INT
- )
-
- val (entities, params) = delegate.extractParams(
- missingParamError = ProcessorErrors
- .UPDATE_MISSING_PARAMS
- )
-
- return UpdateMethod(
- element = delegate.executableElement,
- name = delegate.executableElement.simpleName.toString(),
- entities = entities,
- onConflictStrategy = onConflict,
- returnCount = returnTypeName == TypeName.INT,
- parameters = params
- )
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/cache/Cache.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/cache/Cache.kt
deleted file mode 100644
index 0ef5dde..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/cache/Cache.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.
- */
-
-@file:Suppress("AddVarianceModifier")
-
-package android.arch.persistence.room.processor.cache
-
-import android.arch.persistence.room.processor.FieldProcessor
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.Pojo
-import android.arch.persistence.room.vo.Warning
-import java.util.LinkedHashSet
-import javax.lang.model.element.Element
-import javax.lang.model.type.TypeMirror
-
-/**
- * A cache key can be used to avoid re-processing elements.
- * <p>
- * Each context has a cache variable that uses the same backing storage as the Root Context but
- * adds current adapters and warning suppression list to the key.
- */
-class Cache(val parent: Cache?, val converters: LinkedHashSet<TypeMirror>,
- val suppressedWarnings: Set<Warning>) {
- val entities: Bucket<EntityKey, Entity> = Bucket(parent?.entities)
- val pojos: Bucket<PojoKey, Pojo> = Bucket(parent?.pojos)
-
- inner class Bucket<K, T>(source: Bucket<K, T>?) {
- private val entries: MutableMap<FullKey<K>, T> = source?.entries ?: mutableMapOf()
- fun get(key: K, calculate: () -> T): T {
- val fullKey = FullKey(converters, suppressedWarnings, key)
- return entries.getOrPut(fullKey, {
- calculate()
- })
- }
- }
-
- /**
- * Key for Entity cache
- */
- data class EntityKey(val element: Element)
-
- /**
- * Key for Pojo cache
- */
- data class PojoKey(
- val element: Element,
- val scope: FieldProcessor.BindingScope,
- val parent: EmbeddedField?)
-
- /**
- * Internal key representation with adapters & warnings included.
- * <p>
- * Converters are kept in a linked set since the order is important for the TypeAdapterStore.
- */
- private data class FullKey<T>(
- val converters: LinkedHashSet<TypeMirror>,
- val suppressedWarnings: Set<Warning>,
- val key: T)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/CodeGenScope.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/CodeGenScope.kt
deleted file mode 100644
index 59c2283..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/CodeGenScope.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver
-
-import com.google.common.annotations.VisibleForTesting
-import com.squareup.javapoet.CodeBlock
-import android.arch.persistence.room.writer.ClassWriter
-/**
- * Defines a code generation scope where we can provide temporary variables, global variables etc
- */
-class CodeGenScope(val writer: ClassWriter) {
- private var tmpVarIndices = mutableMapOf<String, Int>()
- private var builder: CodeBlock.Builder? = null
- companion object {
- const val TMP_VAR_DEFAULT_PREFIX = "_tmp"
- const val CLASS_PROPERTY_PREFIX = "__"
- @VisibleForTesting
- fun _tmpVar(index: Int) = _tmpVar(TMP_VAR_DEFAULT_PREFIX, index)
- fun _tmpVar(prefix: String, index: Int) = "$prefix${if (index == 0) "" else "_$index"}"
- }
-
- fun builder(): CodeBlock.Builder {
- if (builder == null) {
- builder = CodeBlock.builder()
- }
- return builder!!
- }
-
- fun getTmpVar(): String {
- return getTmpVar(TMP_VAR_DEFAULT_PREFIX)
- }
-
- fun getTmpVar(prefix: String): String {
- if (!prefix.startsWith("_")) {
- throw IllegalArgumentException("tmp variable prefixes should start with _")
- }
- if (prefix.startsWith(CLASS_PROPERTY_PREFIX)) {
- throw IllegalArgumentException("cannot use $CLASS_PROPERTY_PREFIX for tmp variables")
- }
- val index = tmpVarIndices.getOrElse(prefix) { 0 }
- val result = _tmpVar(prefix, index)
- tmpVarIndices.put(prefix, index + 1)
- return result
- }
-
- fun generate() = builder().build().toString()
-
- /**
- * copies all variable indices but excludes generated code.
- */
- fun fork(): CodeGenScope {
- val forked = CodeGenScope(writer)
- forked.tmpVarIndices.putAll(tmpVarIndices)
- return forked
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt
deleted file mode 100644
index 4bd52cc..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/ObservableQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.solver
-
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.query.result.QueryResultAdapter
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-
-/**
- * Binder provider class that has common functionality for observables.
- */
-abstract class ObservableQueryResultBinderProvider(val context: Context)
- : QueryResultBinderProvider {
- protected abstract fun extractTypeArg(declared: DeclaredType): TypeMirror
- protected abstract fun create(typeArg: TypeMirror,
- resultAdapter: QueryResultAdapter?,
- tableNames: Set<String>): QueryResultBinder
-
- final override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- val typeArg = extractTypeArg(declared)
- val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
- val tableNames = ((adapter?.accessedTableNames() ?: emptyList()) +
- query.tables.map { it.name }).toSet()
- if (tableNames.isEmpty()) {
- context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
- return create(
- typeArg = typeArg,
- resultAdapter = adapter,
- tableNames = tableNames
- )
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/QueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/QueryResultBinderProvider.kt
deleted file mode 100644
index ccc3597..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/QueryResultBinderProvider.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.solver
-
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-
-interface QueryResultBinderProvider {
- fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder
- fun matches(declared: DeclaredType): Boolean
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
deleted file mode 100644
index fc5c4d4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
+++ /dev/null
@@ -1,508 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.GuavaBaseTypeNames
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.EntityProcessor
-import android.arch.persistence.room.processor.FieldProcessor
-import android.arch.persistence.room.processor.PojoProcessor
-import android.arch.persistence.room.solver.binderprovider.CursorQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.DataSourceQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.FlowableQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.InstantQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.LiveDataQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.GuavaListenableFutureQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.RxMaybeQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.RxSingleQueryResultBinderProvider
-import android.arch.persistence.room.solver.query.parameter.ArrayQueryParameterAdapter
-import android.arch.persistence.room.solver.query.parameter.BasicQueryParameterAdapter
-import android.arch.persistence.room.solver.query.parameter.CollectionQueryParameterAdapter
-import android.arch.persistence.room.solver.query.parameter.QueryParameterAdapter
-import android.arch.persistence.room.solver.query.result.ArrayQueryResultAdapter
-import android.arch.persistence.room.solver.query.result.EntityRowAdapter
-import android.arch.persistence.room.solver.query.result.GuavaOptionalQueryResultAdapter
-import android.arch.persistence.room.solver.query.result.InstantQueryResultBinder
-import android.arch.persistence.room.solver.query.result.ListQueryResultAdapter
-import android.arch.persistence.room.solver.query.result.OptionalQueryResultAdapter
-import android.arch.persistence.room.solver.query.result.PojoRowAdapter
-import android.arch.persistence.room.solver.query.result.QueryResultAdapter
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import android.arch.persistence.room.solver.query.result.RowAdapter
-import android.arch.persistence.room.solver.query.result.SingleColumnRowAdapter
-import android.arch.persistence.room.solver.query.result.SingleEntityQueryResultAdapter
-import android.arch.persistence.room.solver.types.BoxedBooleanToBoxedIntConverter
-import android.arch.persistence.room.solver.types.BoxedPrimitiveColumnTypeAdapter
-import android.arch.persistence.room.solver.types.ByteArrayColumnTypeAdapter
-import android.arch.persistence.room.solver.types.ColumnTypeAdapter
-import android.arch.persistence.room.solver.types.CompositeAdapter
-import android.arch.persistence.room.solver.types.CompositeTypeConverter
-import android.arch.persistence.room.solver.types.CursorValueReader
-import android.arch.persistence.room.solver.types.NoOpConverter
-import android.arch.persistence.room.solver.types.PrimitiveBooleanToIntConverter
-import android.arch.persistence.room.solver.types.PrimitiveColumnTypeAdapter
-import android.arch.persistence.room.solver.types.StatementValueBinder
-import android.arch.persistence.room.solver.types.StringColumnTypeAdapter
-import android.arch.persistence.room.solver.types.TypeConverter
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.annotations.VisibleForTesting
-import java.util.LinkedList
-import javax.lang.model.type.ArrayType
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-/**
- * Holds all type adapters and can create on demand composite type adapters to convert a type into a
- * database column.
- */
-class TypeAdapterStore private constructor(
- val context: Context,
- /**
- * first type adapter has the highest priority
- */
- private val columnTypeAdapters: List<ColumnTypeAdapter>,
- /**
- * first converter has the highest priority
- */
- private val typeConverters: List<TypeConverter>) {
-
- companion object {
- fun copy(context: Context, store: TypeAdapterStore): TypeAdapterStore {
- return TypeAdapterStore(context = context,
- columnTypeAdapters = store.columnTypeAdapters,
- typeConverters = store.typeConverters)
- }
-
- fun create(context: Context, @VisibleForTesting vararg extras: Any): TypeAdapterStore {
- val adapters = arrayListOf<ColumnTypeAdapter>()
- val converters = arrayListOf<TypeConverter>()
-
- fun addAny(extra: Any?) {
- when (extra) {
- is TypeConverter -> converters.add(extra)
- is ColumnTypeAdapter -> adapters.add(extra)
- is List<*> -> extra.forEach(::addAny)
- else -> throw IllegalArgumentException("unknown extra $extra")
- }
- }
-
- extras.forEach(::addAny)
- fun addTypeConverter(converter: TypeConverter) {
- converters.add(converter)
- }
-
- fun addColumnAdapter(adapter: ColumnTypeAdapter) {
- adapters.add(adapter)
- }
-
- val primitives = PrimitiveColumnTypeAdapter
- .createPrimitiveAdapters(context.processingEnv)
- primitives.forEach(::addColumnAdapter)
- BoxedPrimitiveColumnTypeAdapter
- .createBoxedPrimitiveAdapters(context.processingEnv, primitives)
- .forEach(::addColumnAdapter)
- addColumnAdapter(StringColumnTypeAdapter(context.processingEnv))
- addColumnAdapter(ByteArrayColumnTypeAdapter(context.processingEnv))
- PrimitiveBooleanToIntConverter.create(context.processingEnv).forEach(::addTypeConverter)
- BoxedBooleanToBoxedIntConverter.create(context.processingEnv)
- .forEach(::addTypeConverter)
- return TypeAdapterStore(context = context, columnTypeAdapters = adapters,
- typeConverters = converters)
- }
- }
-
- val queryResultBinderProviders = listOf(
- CursorQueryResultBinderProvider(context),
- LiveDataQueryResultBinderProvider(context),
- FlowableQueryResultBinderProvider(context),
- GuavaListenableFutureQueryResultBinderProvider(context),
- RxMaybeQueryResultBinderProvider(context),
- RxSingleQueryResultBinderProvider(context),
- DataSourceQueryResultBinderProvider(context),
- DataSourceFactoryQueryResultBinderProvider(context),
- InstantQueryResultBinderProvider(context)
- )
-
- // type mirrors that be converted into columns w/o an extra converter
- private val knownColumnTypeMirrors by lazy {
- columnTypeAdapters.map { it.out }
- }
-
- /**
- * Searches 1 way to bind a value into a statement.
- */
- fun findStatementValueBinder(
- input: TypeMirror,
- affinity: SQLTypeAffinity?
- ): StatementValueBinder? {
- if (input.kind == TypeKind.ERROR) {
- return null
- }
- val adapter = findDirectAdapterFor(input, affinity)
- if (adapter != null) {
- return adapter
- }
- val targetTypes = targetTypeMirrorsFor(affinity)
- val binder = findTypeConverter(input, targetTypes) ?: return null
- // columnAdapter should not be null but we are receiving errors on crash in `first()` so
- // this safeguard allows us to dispatch the real problem to the user (e.g. why we couldn't
- // find the right adapter)
- val columnAdapter = getAllColumnAdapters(binder.to).firstOrNull() ?: return null
- return CompositeAdapter(input, columnAdapter, binder, null)
- }
-
- /**
- * Returns which entities targets the given affinity.
- */
- private fun targetTypeMirrorsFor(affinity: SQLTypeAffinity?): List<TypeMirror> {
- val specifiedTargets = affinity?.getTypeMirrors(context.processingEnv)
- return if (specifiedTargets == null || specifiedTargets.isEmpty()) {
- knownColumnTypeMirrors
- } else {
- specifiedTargets
- }
- }
-
- /**
- * Searches 1 way to read it from cursor
- */
- fun findCursorValueReader(output: TypeMirror, affinity: SQLTypeAffinity?): CursorValueReader? {
- if (output.kind == TypeKind.ERROR) {
- return null
- }
- val adapter = findColumnTypeAdapter(output, affinity)
- if (adapter != null) {
- // two way is better
- return adapter
- }
- // we could not find a two way version, search for anything
- val targetTypes = targetTypeMirrorsFor(affinity)
- val converter = findTypeConverter(targetTypes, output) ?: return null
- return CompositeAdapter(output,
- getAllColumnAdapters(converter.from).first(), null, converter)
- }
-
- /**
- * Tries to reverse the converter going through the same nodes, if possible.
- */
- @VisibleForTesting
- fun reverse(converter: TypeConverter): TypeConverter? {
- return when (converter) {
- is NoOpConverter -> converter
- is CompositeTypeConverter -> {
- val r1 = reverse(converter.conv1) ?: return null
- val r2 = reverse(converter.conv2) ?: return null
- CompositeTypeConverter(r2, r1)
- }
- else -> {
- val types = context.processingEnv.typeUtils
- typeConverters.firstOrNull {
- types.isSameType(it.from, converter.to) && types
- .isSameType(it.to, converter.from)
- }
- }
- }
- }
-
- /**
- * Finds a two way converter, if you need 1 way, use findStatementValueBinder or
- * findCursorValueReader.
- */
- fun findColumnTypeAdapter(out: TypeMirror, affinity: SQLTypeAffinity?): ColumnTypeAdapter? {
- if (out.kind == TypeKind.ERROR) {
- return null
- }
- val adapter = findDirectAdapterFor(out, affinity)
- if (adapter != null) {
- return adapter
- }
- val targetTypes = targetTypeMirrorsFor(affinity)
- val intoStatement = findTypeConverter(out, targetTypes) ?: return null
- // ok found a converter, try the reverse now
- val fromCursor = reverse(intoStatement) ?: findTypeConverter(intoStatement.to, out)
- ?: return null
- return CompositeAdapter(out, getAllColumnAdapters(intoStatement.to).first(), intoStatement,
- fromCursor)
- }
-
- private fun findDirectAdapterFor(
- out: TypeMirror, affinity: SQLTypeAffinity?): ColumnTypeAdapter? {
- val adapter = getAllColumnAdapters(out).firstOrNull {
- affinity == null || it.typeAffinity == affinity
- }
- return adapter
- }
-
- fun findTypeConverter(input: TypeMirror, output: TypeMirror): TypeConverter? {
- return findTypeConverter(listOf(input), listOf(output))
- }
-
- fun findQueryResultBinder(typeMirror: TypeMirror, query: ParsedQuery): QueryResultBinder {
- return if (typeMirror.kind == TypeKind.DECLARED) {
- val declared = MoreTypes.asDeclared(typeMirror)
- return queryResultBinderProviders.first {
- it.matches(declared)
- }.provide(declared, query)
- } else {
- InstantQueryResultBinder(findQueryResultAdapter(typeMirror, query))
- }
- }
-
- fun findQueryResultAdapter(typeMirror: TypeMirror, query: ParsedQuery): QueryResultAdapter? {
- if (typeMirror.kind == TypeKind.ERROR) {
- return null
- }
- if (typeMirror.kind == TypeKind.DECLARED) {
- val declared = MoreTypes.asDeclared(typeMirror)
-
- if (declared.typeArguments.isEmpty()) {
- val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
- return SingleEntityQueryResultAdapter(rowAdapter)
- } else if (
- context.processingEnv.typeUtils.erasure(typeMirror).typeName() ==
- GuavaBaseTypeNames.OPTIONAL) {
- // Handle Guava Optional by unpacking its generic type argument and adapting that.
- // The Optional adapter will reappend the Optional type.
- val typeArg = declared.typeArguments.first()
- val rowAdapter = findRowAdapter(typeArg, query) ?: return null
- return GuavaOptionalQueryResultAdapter(SingleEntityQueryResultAdapter(rowAdapter))
- } else if (
- context.processingEnv.typeUtils.erasure(typeMirror).typeName() ==
- CommonTypeNames.OPTIONAL) {
- // Handle java.util.Optional similarly.
- val typeArg = declared.typeArguments.first()
- val rowAdapter = findRowAdapter(typeArg, query) ?: return null
- return OptionalQueryResultAdapter(SingleEntityQueryResultAdapter(rowAdapter))
- } else if (MoreTypes.isTypeOf(java.util.List::class.java, typeMirror)) {
- val typeArg = declared.typeArguments.first()
- val rowAdapter = findRowAdapter(typeArg, query) ?: return null
- return ListQueryResultAdapter(rowAdapter)
- }
- return null
- } else if (typeMirror is ArrayType && typeMirror.componentType.kind != TypeKind.BYTE) {
- val rowAdapter =
- findRowAdapter(typeMirror.componentType, query) ?: return null
- return ArrayQueryResultAdapter(rowAdapter)
- } else {
- val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
- return SingleEntityQueryResultAdapter(rowAdapter)
- }
- }
-
- /**
- * Find a converter from cursor to the given type mirror.
- * If there is information about the query result, we try to use it to accept *any* POJO.
- */
- @VisibleForTesting
- fun findRowAdapter(typeMirror: TypeMirror, query: ParsedQuery): RowAdapter? {
- if (typeMirror.kind == TypeKind.ERROR) {
- return null
- }
- if (typeMirror.kind == TypeKind.DECLARED) {
- val declared = MoreTypes.asDeclared(typeMirror)
- if (declared.typeArguments.isNotEmpty()) {
- // TODO one day support this
- return null
- }
- val resultInfo = query.resultInfo
-
- val (rowAdapter, rowAdapterLogs) = if (resultInfo != null && query.errors.isEmpty()
- && resultInfo.error == null) {
- // if result info is not null, first try a pojo row adapter
- context.collectLogs { subContext ->
- val pojo = PojoProcessor(
- baseContext = subContext,
- element = MoreTypes.asTypeElement(typeMirror),
- bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
- parent = null
- ).process()
- PojoRowAdapter(
- context = subContext,
- info = resultInfo,
- pojo = pojo,
- out = typeMirror)
- }
- } else {
- Pair(null, null)
- }
-
- if (rowAdapter == null && query.resultInfo == null) {
- // we don't know what query returns. Check for entity.
- val asElement = MoreTypes.asElement(typeMirror)
- if (asElement.hasAnnotation(Entity::class)) {
- return EntityRowAdapter(EntityProcessor(context,
- MoreElements.asType(asElement)).process())
- }
- }
-
- if (rowAdapter != null && rowAdapterLogs?.hasErrors() != true) {
- rowAdapterLogs?.writeTo(context.processingEnv)
- return rowAdapter
- }
-
- if ((resultInfo?.columns?.size ?: 1) == 1) {
- val singleColumn = findCursorValueReader(typeMirror,
- resultInfo?.columns?.get(0)?.type)
- if (singleColumn != null) {
- return SingleColumnRowAdapter(singleColumn)
- }
- }
- // if we tried, return its errors
- if (rowAdapter != null) {
- rowAdapterLogs?.writeTo(context.processingEnv)
- return rowAdapter
- }
- if (query.runtimeQueryPlaceholder) {
- // just go w/ pojo and hope for the best. this happens for @RawQuery where we
- // try to guess user's intention and hope that their query fits the result.
- val pojo = PojoProcessor(
- baseContext = context,
- element = MoreTypes.asTypeElement(typeMirror),
- bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
- parent = null
- ).process()
- return PojoRowAdapter(
- context = context,
- info = null,
- pojo = pojo,
- out = typeMirror)
- }
- return null
- } else {
- val singleColumn = findCursorValueReader(typeMirror, null) ?: return null
- return SingleColumnRowAdapter(singleColumn)
- }
- }
-
- fun findQueryParameterAdapter(typeMirror: TypeMirror): QueryParameterAdapter? {
- if (MoreTypes.isType(typeMirror)
- && (MoreTypes.isTypeOf(java.util.List::class.java, typeMirror)
- || MoreTypes.isTypeOf(java.util.Set::class.java, typeMirror))) {
- val declared = MoreTypes.asDeclared(typeMirror)
- val binder = findStatementValueBinder(declared.typeArguments.first(),
- null)
- if (binder != null) {
- return CollectionQueryParameterAdapter(binder)
- } else {
- // maybe user wants to convert this collection themselves. look for a match
- val collectionBinder = findStatementValueBinder(typeMirror, null) ?: return null
- return BasicQueryParameterAdapter(collectionBinder)
- }
- } else if (typeMirror is ArrayType && typeMirror.componentType.kind != TypeKind.BYTE) {
- val component = typeMirror.componentType
- val binder = findStatementValueBinder(component, null) ?: return null
- return ArrayQueryParameterAdapter(binder)
- } else {
- val binder = findStatementValueBinder(typeMirror, null) ?: return null
- return BasicQueryParameterAdapter(binder)
- }
- }
-
- private fun findTypeConverter(input: TypeMirror, outputs: List<TypeMirror>): TypeConverter? {
- return findTypeConverter(listOf(input), outputs)
- }
-
- private fun findTypeConverter(input: List<TypeMirror>, output: TypeMirror): TypeConverter? {
- return findTypeConverter(input, listOf(output))
- }
-
- private fun findTypeConverter(
- inputs: List<TypeMirror>, outputs: List<TypeMirror>): TypeConverter? {
- if (inputs.isEmpty()) {
- return null
- }
- val types = context.processingEnv.typeUtils
- inputs.forEach { input ->
- if (outputs.any { output -> types.isSameType(input, output) }) {
- return NoOpConverter(input)
- }
- }
-
- val excludes = arrayListOf<TypeMirror>()
-
- val queue = LinkedList<TypeConverter>()
- fun exactMatch(candidates: List<TypeConverter>): TypeConverter? {
- return candidates.firstOrNull {
- outputs.any { output -> types.isSameType(output, it.to) }
- }
- }
- inputs.forEach { input ->
- val candidates = getAllTypeConverters(input, excludes)
- val match = exactMatch(candidates)
- if (match != null) {
- return match
- }
- candidates.forEach {
- excludes.add(it.to)
- queue.add(it)
- }
- }
- excludes.addAll(inputs)
- while (queue.isNotEmpty()) {
- val prev = queue.pop()
- val from = prev.to
- val candidates = getAllTypeConverters(from, excludes)
- val match = exactMatch(candidates)
- if (match != null) {
- return CompositeTypeConverter(prev, match)
- }
- candidates.forEach {
- excludes.add(it.to)
- queue.add(CompositeTypeConverter(prev, it))
- }
- }
- return null
- }
-
- private fun getAllColumnAdapters(input: TypeMirror): List<ColumnTypeAdapter> {
- return columnTypeAdapters.filter {
- context.processingEnv.typeUtils.isSameType(input, it.out)
- }
- }
-
- /**
- * Returns all type converters that can receive input type and return into another type.
- * The returned list is ordered by priority such that if we have an exact match, it is
- * prioritized.
- */
- private fun getAllTypeConverters(input: TypeMirror, excludes: List<TypeMirror>):
- List<TypeConverter> {
- val types = context.processingEnv.typeUtils
- // for input, check assignability because it defines whether we can use the method or not.
- // for excludes, use exact match
- return typeConverters.filter { converter ->
- types.isAssignable(input, converter.from) &&
- !excludes.any { types.isSameType(it, converter.to) }
- }.sortedByDescending {
- // if it is the same, prioritize
- if (types.isSameType(it.from, input)) {
- 2
- } else {
- 1
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/CursorQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/CursorQueryResultBinderProvider.kt
deleted file mode 100644
index e160672..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/CursorQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.CursorQueryResultBinder
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import com.squareup.javapoet.TypeName
-import javax.lang.model.type.DeclaredType
-
-class CursorQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
- override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- return CursorQueryResultBinder()
- }
-
- override fun matches(declared: DeclaredType): Boolean =
- declared.typeArguments.size == 0 && TypeName.get(declared) == AndroidTypeNames.CURSOR
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
deleted file mode 100644
index c4fb238..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.ListQueryResultAdapter
-import android.arch.persistence.room.solver.query.result.DataSourceFactoryQueryResultBinder
-import android.arch.persistence.room.solver.query.result.PositionalDataSourceQueryResultBinder
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-
-class DataSourceFactoryQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
- private val dataSourceFactoryTypeMirror: TypeMirror? by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY.toString())?.asType()
- }
-
- override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- if (query.tables.isEmpty()) {
- context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
- val typeArg = declared.typeArguments[1]
- val adapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
- ListQueryResultAdapter(it)
- }
-
- val tableNames = ((adapter?.accessedTableNames() ?: emptyList())
- + query.tables.map { it.name }).toSet()
- val countedBinder = PositionalDataSourceQueryResultBinder(adapter, tableNames)
- return DataSourceFactoryQueryResultBinder(countedBinder)
- }
-
- override fun matches(declared: DeclaredType): Boolean =
- declared.typeArguments.size == 2 && isLivePagedList(declared)
-
- private fun isLivePagedList(declared: DeclaredType): Boolean {
- if (dataSourceFactoryTypeMirror == null) {
- return false
- }
- val erasure = context.processingEnv.typeUtils.erasure(declared)
- // we don't want to return paged list unless explicitly requested
- return context.processingEnv.typeUtils.isAssignable(dataSourceFactoryTypeMirror, erasure)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
deleted file mode 100644
index 56eac6b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.PositionalDataSourceQueryResultBinder
-import android.arch.persistence.room.solver.query.result.ListQueryResultAdapter
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-
-class DataSourceQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
- private val dataSourceTypeMirror: TypeMirror? by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(PagingTypeNames.DATA_SOURCE.toString())?.asType()
- }
-
- private val positionalDataSourceTypeMirror: TypeMirror? by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(PagingTypeNames.POSITIONAL_DATA_SOURCE.toString())?.asType()
- }
-
- override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- if (query.tables.isEmpty()) {
- context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
- val typeArg = declared.typeArguments.last()
- val listAdapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
- ListQueryResultAdapter(it)
- }
- val tableNames = ((listAdapter?.accessedTableNames() ?: emptyList())
- + query.tables.map { it.name }).toSet()
- return PositionalDataSourceQueryResultBinder(listAdapter, tableNames)
- }
-
- override fun matches(declared: DeclaredType): Boolean {
- if (dataSourceTypeMirror == null || positionalDataSourceTypeMirror == null) {
- return false
- }
- if (declared.typeArguments.isEmpty()) {
- return false
- }
- val erasure = context.processingEnv.typeUtils.erasure(declared)
- val isDataSource = context.processingEnv.typeUtils
- .isAssignable(erasure, dataSourceTypeMirror)
- if (!isDataSource) {
- return false
- }
- val isPositional = context.processingEnv.typeUtils
- .isAssignable(erasure, positionalDataSourceTypeMirror)
- if (!isPositional) {
- context.logger.e(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
- }
- return true
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/FlowableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/FlowableQueryResultBinderProvider.kt
deleted file mode 100644
index 6a2c901..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/FlowableQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.RoomRxJava2TypeNames
-import android.arch.persistence.room.ext.RxJava2TypeNames
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.ObservableQueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.FlowableQueryResultBinder
-import android.arch.persistence.room.solver.query.result.QueryResultAdapter
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-
-class FlowableQueryResultBinderProvider(context: Context) :
- ObservableQueryResultBinderProvider(context) {
- private val flowableTypeMirror: TypeMirror? by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(RxJava2TypeNames.FLOWABLE.toString())?.asType()
- }
-
- private val hasRxJava2Artifact by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(RoomRxJava2TypeNames.RX_ROOM.toString()) != null
- }
-
- override fun extractTypeArg(declared: DeclaredType): TypeMirror = declared.typeArguments.first()
-
- override fun create(typeArg: TypeMirror, resultAdapter: QueryResultAdapter?,
- tableNames: Set<String>): QueryResultBinder {
- return FlowableQueryResultBinder(
- typeArg = typeArg,
- queryTableNames = tableNames,
- adapter = resultAdapter)
- }
-
- override fun matches(declared: DeclaredType): Boolean =
- declared.typeArguments.size == 1 && isRxJava2Publisher(declared)
-
- private fun isRxJava2Publisher(declared: DeclaredType): Boolean {
- if (flowableTypeMirror == null) {
- return false
- }
- val erasure = context.processingEnv.typeUtils.erasure(declared)
- val match = context.processingEnv.typeUtils.isAssignable(flowableTypeMirror, erasure)
- if (match && !hasRxJava2Artifact) {
- context.logger.e(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
- }
- return match
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/GuavaListenableFutureQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/GuavaListenableFutureQueryResultBinderProvider.kt
deleted file mode 100644
index 7876a63..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/GuavaListenableFutureQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2018 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.GuavaUtilConcurrentTypeNames
-import android.arch.persistence.room.ext.RoomGuavaTypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import android.arch.persistence.room.solver.query.result.GuavaListenableFutureQueryResultBinder
-import javax.lang.model.type.DeclaredType
-
-class GuavaListenableFutureQueryResultBinderProvider(val context: Context)
- : QueryResultBinderProvider {
-
- private val hasGuavaRoom by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(RoomGuavaTypeNames.GUAVA_ROOM.toString()) != null
- }
-
- /**
- * Returns the {@link GuavaListenableFutureQueryResultBinder} instance for the input type, if
- * possible.
- *
- * <p>Emits a compiler error if the Guava Room extension library is not linked.
- */
- override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- if (!hasGuavaRoom) {
- context.logger.e(ProcessorErrors.MISSING_ROOM_GUAVA_ARTIFACT)
- }
-
- // Use the type T inside ListenableFuture<T> as the type to adapt and to pass into
- // the binder.
- val adapter = context.typeAdapterStore.findQueryResultAdapter(
- declared.typeArguments.first(), query)
- return GuavaListenableFutureQueryResultBinder(
- declared.typeArguments.first(), adapter)
- }
-
- /**
- * Returns true iff the input {@code declared} type is ListenableFuture<T>.
- */
- override fun matches(declared: DeclaredType): Boolean =
- declared.typeArguments.size == 1 &&
- context.processingEnv.typeUtils.erasure(declared).typeName() ==
- GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/InstantQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/InstantQueryResultBinderProvider.kt
deleted file mode 100644
index c7d1e2d..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/InstantQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.InstantQueryResultBinder
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-
-class InstantQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
- override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- return InstantQueryResultBinder(
- context.typeAdapterStore.findQueryResultAdapter(declared, query))
- }
-
- override fun matches(declared: DeclaredType): Boolean = true
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/LiveDataQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/LiveDataQueryResultBinderProvider.kt
deleted file mode 100644
index b62eabd..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/LiveDataQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.LifecyclesTypeNames
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.solver.ObservableQueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.LiveDataQueryResultBinder
-import android.arch.persistence.room.solver.query.result.QueryResultAdapter
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeMirror
-
-class LiveDataQueryResultBinderProvider(context: Context)
- : ObservableQueryResultBinderProvider(context) {
- private val liveDataTypeMirror: TypeMirror? by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString())?.asType()
- }
-
- override fun extractTypeArg(declared: DeclaredType): TypeMirror = declared.typeArguments.first()
-
- override fun create(typeArg: TypeMirror, resultAdapter: QueryResultAdapter?,
- tableNames: Set<String>): QueryResultBinder {
- return LiveDataQueryResultBinder(
- typeArg = typeArg,
- tableNames = tableNames,
- adapter = resultAdapter)
- }
-
- override fun matches(declared: DeclaredType): Boolean =
- declared.typeArguments.size == 1 && isLiveData(declared)
-
- private fun isLiveData(declared: DeclaredType): Boolean {
- if (liveDataTypeMirror == null) {
- return false
- }
- val erasure = context.processingEnv.typeUtils.erasure(declared)
- return context.processingEnv.typeUtils.isAssignable(liveDataTypeMirror, erasure)
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
deleted file mode 100644
index 11b824f..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.solver.binderprovider
-
-import android.arch.persistence.room.ext.RoomRxJava2TypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import android.arch.persistence.room.solver.query.result.RxCallableQueryResultBinder
-import javax.lang.model.type.DeclaredType
-
-sealed class RxCallableQueryResultBinderProvider(val context: Context,
- val rxType: RxCallableQueryResultBinder.RxType)
- : QueryResultBinderProvider {
- private val hasRxJava2Artifact by lazy {
- context.processingEnv.elementUtils
- .getTypeElement(RoomRxJava2TypeNames.RX_ROOM.toString()) != null
- }
-
- override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
- val typeArg = declared.typeArguments.first()
- val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
- return RxCallableQueryResultBinder(rxType, typeArg, adapter)
- }
-
- override fun matches(declared: DeclaredType): Boolean =
- declared.typeArguments.size == 1 && matchesRxType(declared)
-
- private fun matchesRxType(declared: DeclaredType): Boolean {
- val erasure = context.processingEnv.typeUtils.erasure(declared)
- val match = erasure.typeName() == rxType.className
- if (match && !hasRxJava2Artifact) {
- context.logger.e(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
- }
- return match
- }
-}
-
-class RxSingleQueryResultBinderProvider(context: Context)
- : RxCallableQueryResultBinderProvider(context, RxCallableQueryResultBinder.RxType.SINGLE)
-
-class RxMaybeQueryResultBinderProvider(context: Context)
- : RxCallableQueryResultBinderProvider(context, RxCallableQueryResultBinder.RxType.MAYBE)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/ArrayQueryParameterAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/ArrayQueryParameterAdapter.kt
deleted file mode 100644
index 12756d8..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/ArrayQueryParameterAdapter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.parameter
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.solver.types.StatementValueBinder
-import com.squareup.javapoet.TypeName
-
-/**
- * Binds ARRAY(T) (e.g. int[]) into String[] args of a query.
- */
-class ArrayQueryParameterAdapter(val bindAdapter: StatementValueBinder)
- : QueryParameterAdapter(true) {
- override fun bindToStmt(inputVarName: String, stmtVarName: String, startIndexVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- val itrVar = scope.getTmpVar("_item")
- beginControlFlow("for ($T $L : $L)", bindAdapter.typeMirror().typeName(), itrVar,
- inputVarName).apply {
- bindAdapter.bindToStmt(stmtVarName, startIndexVarName, itrVar, scope)
- addStatement("$L ++", startIndexVarName)
- }
- endControlFlow()
- }
- }
-
- override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
- scope.builder()
- .addStatement("final $T $L = $L.length", TypeName.INT, outputVarName, inputVarName)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/BasicQueryParameterAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/BasicQueryParameterAdapter.kt
deleted file mode 100644
index 41ab2cf..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/BasicQueryParameterAdapter.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.parameter
-
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.solver.types.StatementValueBinder
-
-/**
- * Knows how to convert a query parameter into arguments
- */
-class BasicQueryParameterAdapter(val bindAdapter: StatementValueBinder)
- : QueryParameterAdapter(false) {
- override fun bindToStmt(inputVarName: String, stmtVarName: String, startIndexVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- bindAdapter.bindToStmt(stmtVarName, startIndexVarName, inputVarName, scope)
- }
- }
-
- override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
- throw UnsupportedOperationException("should not call getArgCount on basic adapters." +
- "It is always one.")
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/CollectionQueryParameterAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/CollectionQueryParameterAdapter.kt
deleted file mode 100644
index 00c350a..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/CollectionQueryParameterAdapter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.parameter
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.solver.types.StatementValueBinder
-import com.squareup.javapoet.TypeName
-
-/**
- * Binds Collection<T> (e.g. List<T>) into String[] query args.
- */
-class CollectionQueryParameterAdapter(val bindAdapter: StatementValueBinder)
- : QueryParameterAdapter(true) {
- override fun bindToStmt(inputVarName: String, stmtVarName: String, startIndexVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- val itrVar = scope.getTmpVar("_item")
- beginControlFlow("for ($T $L : $L)", bindAdapter.typeMirror().typeName(), itrVar,
- inputVarName).apply {
- bindAdapter.bindToStmt(stmtVarName, startIndexVarName, itrVar, scope)
- addStatement("$L ++", startIndexVarName)
- }
- endControlFlow()
- }
- }
-
- override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
- scope.builder()
- .addStatement("final $T $L = $L.size()", TypeName.INT, outputVarName, inputVarName)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/QueryParameterAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/QueryParameterAdapter.kt
deleted file mode 100644
index a42bfee..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/parameter/QueryParameterAdapter.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.parameter
-
-import android.arch.persistence.room.solver.CodeGenScope
-
-/**
- * Knows how to convert a query parameter into query arguments.
- */
-abstract class QueryParameterAdapter(val isMultiple: Boolean) {
- /**
- * Must bind the value into the statement at the given index.
- */
- abstract fun bindToStmt(
- inputVarName: String,
- stmtVarName: String,
- startIndexVarName: String,
- scope: CodeGenScope)
-
- /**
- * Should declare and set the given value with the count
- */
- abstract fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/ArrayQueryResultAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/ArrayQueryResultAdapter.kt
deleted file mode 100644
index e8330bc..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/ArrayQueryResultAdapter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.TypeName
-
-class ArrayQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
- val type = rowAdapter.out
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- rowAdapter?.onCursorReady(cursorVarName, scope)
-
- val arrayType = ArrayTypeName.of(type.typeName())
- addStatement("final $T $L = new $T[$L.getCount()]",
- arrayType, outVarName, type.typeName(), cursorVarName)
- val tmpVarName = scope.getTmpVar("_item")
- val indexVar = scope.getTmpVar("_index")
- addStatement("$T $L = 0", TypeName.INT, indexVar)
- beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
- addStatement("final $T $L", type.typeName(), tmpVarName)
- rowAdapter?.convert(tmpVarName, cursorVarName, scope)
- addStatement("$L[$L] = $L", outVarName, indexVar, tmpVarName)
- addStatement("$L ++", indexVar)
- }
- endControlFlow()
- rowAdapter?.onCursorFinished()?.invoke(scope)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
deleted file mode 100644
index 883180c..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.writer.DaoWriter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import javax.lang.model.element.Modifier
-
-/**
- * Base class for query result binders that observe the database. It includes common functionality
- * like creating a finalizer to release the query or creating the actual adapter call code.
- */
-abstract class BaseObservableQueryResultBinder(adapter: QueryResultAdapter?)
- : QueryResultBinder(adapter) {
-
- protected fun createFinalizeMethod(roomSQLiteQueryVar: String): MethodSpec {
- return MethodSpec.methodBuilder("finalize").apply {
- addModifiers(Modifier.PROTECTED)
- addAnnotation(Override::class.java)
- addStatement("$L.release()", roomSQLiteQueryVar)
- }.build()
- }
-
- protected fun createRunQueryAndReturnStatements(builder: MethodSpec.Builder,
- roomSQLiteQueryVar: String,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- val transactionWrapper = if (inTransaction) {
- builder.transactionWrapper(dbField)
- } else {
- null
- }
- val outVar = scope.getTmpVar("_result")
- val cursorVar = scope.getTmpVar("_cursor")
- transactionWrapper?.beginTransactionWithControlFlow()
- builder.apply {
- addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
- DaoWriter.dbField, roomSQLiteQueryVar)
- beginControlFlow("try").apply {
- val adapterScope = scope.fork()
- adapter?.convert(outVar, cursorVar, adapterScope)
- addCode(adapterScope.builder().build())
- transactionWrapper?.commitTransaction()
- addStatement("return $L", outVar)
- }
- nextControlFlow("finally").apply {
- addStatement("$L.close()", cursorVar)
- }
- endControlFlow()
- }
- transactionWrapper?.endTransactionWithControlFlow()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
deleted file mode 100644
index 7aa24cc..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.FieldSpec
-
-class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
- override fun convertAndReturn(roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- val builder = scope.builder()
- val transactionWrapper = if (inTransaction) {
- builder.transactionWrapper(dbField)
- } else {
- null
- }
- transactionWrapper?.beginTransactionWithControlFlow()
- val resultName = scope.getTmpVar("_tmpResult")
- builder.addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, resultName,
- dbField, roomSQLiteQueryVar)
- transactionWrapper?.commitTransaction()
- builder.addStatement("return $L", resultName)
- transactionWrapper?.endTransactionWithControlFlow()
- }
-
- companion object {
- private val NO_OP_RESULT_ADAPTER = object : QueryResultAdapter(null) {
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
deleted file mode 100644
index 93b2d7b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-
-class DataSourceFactoryQueryResultBinder(
- val positionalDataSourceQueryResultBinder: PositionalDataSourceQueryResultBinder)
- : QueryResultBinder(positionalDataSourceQueryResultBinder.listAdapter) {
- @Suppress("HasPlatformType")
- val typeName = positionalDataSourceQueryResultBinder.itemTypeName
- override fun convertAndReturn(
- roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope
- ) {
- scope.builder().apply {
- val pagedListProvider = TypeSpec
- .anonymousClassBuilder("").apply {
- superclass(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY,
- Integer::class.typeName(), typeName))
- addMethod(createCreateMethod(
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- dbField = dbField,
- inTransaction = inTransaction,
- scope = scope))
- }.build()
- addStatement("return $L", pagedListProvider)
- }
- }
-
- private fun createCreateMethod(
- roomSQLiteQueryVar: String,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope
- ): MethodSpec = MethodSpec.methodBuilder("create").apply {
- addAnnotation(Override::class.java)
- addModifiers(Modifier.PUBLIC)
- returns(positionalDataSourceQueryResultBinder.typeName)
- val countedBinderScope = scope.fork()
- positionalDataSourceQueryResultBinder.convertAndReturn(
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- canReleaseQuery = true,
- dbField = dbField,
- inTransaction = inTransaction,
- scope = countedBinderScope)
- addCode(countedBinderScope.builder().build())
- }.build()
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/EntityRowAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/EntityRowAdapter.kt
deleted file mode 100644
index c97f69e..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/EntityRowAdapter.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.writer.EntityCursorConverterWriter
-import com.squareup.javapoet.MethodSpec
-
-class EntityRowAdapter(val entity: Entity) : RowAdapter(entity.type) {
- lateinit var methodSpec: MethodSpec
- override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
- methodSpec = scope.writer.getOrCreateMethod(EntityCursorConverterWriter(entity))
- }
-
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder()
- .addStatement("$L = $N($L)", outVarName, methodSpec, cursorVarName)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
deleted file mode 100644
index 0fb21b1..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomRxJava2TypeNames
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.arrayTypeName
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.writer.DaoWriter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-import javax.lang.model.type.TypeMirror
-
-/**
- * Binds the result as an RxJava2 Flowable.
- */
-class FlowableQueryResultBinder(val typeArg: TypeMirror, val queryTableNames: Set<String>,
- adapter: QueryResultAdapter?)
- : BaseObservableQueryResultBinder(adapter) {
- override fun convertAndReturn(roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- val callableImpl = TypeSpec.anonymousClassBuilder("").apply {
- val typeName = typeArg.typeName()
- superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
- typeName))
- addMethod(MethodSpec.methodBuilder("call").apply {
- returns(typeName)
- addException(Exception::class.typeName())
- addModifiers(Modifier.PUBLIC)
- addAnnotation(Override::class.java)
- createRunQueryAndReturnStatements(builder = this,
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- inTransaction = inTransaction,
- dbField = dbField,
- scope = scope)
- }.build())
- if (canReleaseQuery) {
- addMethod(createFinalizeMethod(roomSQLiteQueryVar))
- }
- }.build()
- scope.builder().apply {
- val tableNamesList = queryTableNames.joinToString(",") { "\"$it\"" }
- addStatement("return $T.createFlowable($N, new $T{$L}, $L)",
- RoomRxJava2TypeNames.RX_ROOM, DaoWriter.dbField,
- String::class.arrayTypeName(), tableNamesList, callableImpl)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
deleted file mode 100644
index de57d78..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2018 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.solver.query.result
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomGuavaTypeNames
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.writer.DaoWriter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-import javax.lang.model.type.TypeMirror
-
-/**
- * A ResultBinder that emits a ListenableFuture<T> where T is the input {@code typeArg}.
- *
- * <p>The Future runs on the background thread Executor.
- */
-class GuavaListenableFutureQueryResultBinder(
- val typeArg: TypeMirror,
- adapter: QueryResultAdapter?)
- : BaseObservableQueryResultBinder(adapter) {
-
- override fun convertAndReturn(
- roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- // Callable<T>
- val callableImpl = createCallableOfT(
- roomSQLiteQueryVar,
- dbField,
- inTransaction,
- scope)
-
- scope.builder().apply {
- addStatement(
- "return $T.createListenableFuture($L, $L, $L)",
- RoomGuavaTypeNames.GUAVA_ROOM,
- callableImpl,
- roomSQLiteQueryVar,
- canReleaseQuery)
- }
- }
-
- /**
- * Returns an anonymous subclass of Callable<T> that executes the database transaction and
- * constitutes the result T.
- *
- * <p>Note that this method does not release the query object.
- */
- private fun createCallableOfT(
- roomSQLiteQueryVar: String,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope): TypeSpec {
- return TypeSpec.anonymousClassBuilder("").apply {
- superclass(
- ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
- typeArg.typeName()))
- addMethod(
- MethodSpec.methodBuilder("call").apply {
- // public T call() throws Exception {}
- returns(typeArg.typeName())
- addAnnotation(Override::class.typeName())
- addModifiers(Modifier.PUBLIC)
- addException(Exception::class.typeName())
-
- // Body.
- val transactionWrapper = if (inTransaction) {
- transactionWrapper(dbField)
- } else {
- null
- }
- transactionWrapper?.beginTransactionWithControlFlow()
- apply {
- val outVar = scope.getTmpVar("_result")
- val cursorVar = scope.getTmpVar("_cursor")
- addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR,
- cursorVar,
- DaoWriter.dbField, roomSQLiteQueryVar)
- beginControlFlow("try").apply {
- val adapterScope = scope.fork()
- adapter?.convert(outVar, cursorVar, adapterScope)
- addCode(adapterScope.builder().build())
- transactionWrapper?.commitTransaction()
- addStatement("return $L", outVar)
- }
- nextControlFlow("finally").apply {
- addStatement("$L.close()", cursorVar)
- }
- endControlFlow()
- }
- transactionWrapper?.endTransactionWithControlFlow()
- }.build())
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
deleted file mode 100644
index 33fb37b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2018 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.solver.query.result
-
-import android.arch.persistence.room.ext.GuavaBaseTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.ParameterizedTypeName
-
-/**
- * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
- * {@link com.google.common.base.Optional}.
- */
-class GuavaOptionalQueryResultAdapter(private val resultAdapter: SingleEntityQueryResultAdapter)
- : QueryResultAdapter(resultAdapter.rowAdapter) {
- val type = resultAdapter.rowAdapter?.out
- override fun convert(
- outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- val valueVarName = scope.getTmpVar("_value")
- resultAdapter?.convert(valueVarName, cursorVarName, scope)
- addStatement(
- "final $T $L = $T.fromNullable($L)",
- ParameterizedTypeName.get(GuavaBaseTypeNames.OPTIONAL, type?.typeName()),
- outVarName,
- GuavaBaseTypeNames.OPTIONAL,
- valueVarName)
- }
- }
-}
-
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
deleted file mode 100644
index aa64b1b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.writer.DaoWriter
-import com.squareup.javapoet.FieldSpec
-
-/**
- * Instantly runs and returns the query.
- */
-class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
- override fun convertAndReturn(roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- val transactionWrapper = if (inTransaction) {
- scope.builder().transactionWrapper(dbField)
- } else {
- null
- }
- transactionWrapper?.beginTransactionWithControlFlow()
- scope.builder().apply {
- val outVar = scope.getTmpVar("_result")
- val cursorVar = scope.getTmpVar("_cursor")
- addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
- DaoWriter.dbField, roomSQLiteQueryVar)
- beginControlFlow("try").apply {
- adapter?.convert(outVar, cursorVar, scope)
- transactionWrapper?.commitTransaction()
- addStatement("return $L", outVar)
- }
- nextControlFlow("finally").apply {
- addStatement("$L.close()", cursorVar)
- if (canReleaseQuery) {
- addStatement("$L.release()", roomSQLiteQueryVar)
- }
- }
- endControlFlow()
- }
- transactionWrapper?.endTransactionWithControlFlow()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/ListQueryResultAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/ListQueryResultAdapter.kt
deleted file mode 100644
index b70ec5a..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/ListQueryResultAdapter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import java.util.ArrayList
-
-class ListQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
- val type = rowAdapter.out
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- rowAdapter?.onCursorReady(cursorVarName, scope)
- val collectionType = ParameterizedTypeName
- .get(ClassName.get(List::class.java), type.typeName())
- val arrayListType = ParameterizedTypeName
- .get(ClassName.get(ArrayList::class.java), type.typeName())
- addStatement("final $T $L = new $T($L.getCount())",
- collectionType, outVarName, arrayListType, cursorVarName)
- val tmpVarName = scope.getTmpVar("_item")
- beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
- addStatement("final $T $L", type.typeName(), tmpVarName)
- rowAdapter?.convert(tmpVarName, cursorVarName, scope)
- addStatement("$L.add($L)", outVarName, tmpVarName)
- }
- endControlFlow()
- rowAdapter?.onCursorFinished()?.invoke(scope)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
deleted file mode 100644
index 416b8b8..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.LifecyclesTypeNames
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.RoomTypeNames.INVALIDATION_OBSERVER
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import android.support.annotation.NonNull
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-import javax.lang.model.type.TypeMirror
-
-/**
- * Converts the query into a LiveData and returns it. No query is run until necessary.
- */
-class LiveDataQueryResultBinder(val typeArg: TypeMirror, val tableNames: Set<String>,
- adapter: QueryResultAdapter?)
- : BaseObservableQueryResultBinder(adapter) {
- @Suppress("JoinDeclarationAndAssignment")
- override fun convertAndReturn(
- roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope
- ) {
- val typeName = typeArg.typeName()
-
- val liveDataImpl = TypeSpec.anonymousClassBuilder("").apply {
- superclass(ParameterizedTypeName.get(LifecyclesTypeNames.COMPUTABLE_LIVE_DATA,
- typeName))
- val observerField = FieldSpec.builder(RoomTypeNames.INVALIDATION_OBSERVER,
- scope.getTmpVar("_observer"), Modifier.PRIVATE).build()
- addField(observerField)
- addMethod(createComputeMethod(
- observerField = observerField,
- typeName = typeName,
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- dbField = dbField,
- inTransaction = inTransaction,
- scope = scope
- ))
- if (canReleaseQuery) {
- addMethod(createFinalizeMethod(roomSQLiteQueryVar))
- }
- }.build()
- scope.builder().apply {
- addStatement("return $L.getLiveData()", liveDataImpl)
- }
- }
-
- private fun createComputeMethod(
- roomSQLiteQueryVar: String,
- typeName: TypeName,
- observerField: FieldSpec,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope
- ): MethodSpec {
- return MethodSpec.methodBuilder("compute").apply {
- addAnnotation(Override::class.java)
- addModifiers(Modifier.PROTECTED)
- returns(typeName)
-
- beginControlFlow("if ($N == null)", observerField).apply {
- addStatement("$N = $L", observerField, createAnonymousObserver())
- addStatement("$N.getInvalidationTracker().addWeakObserver($N)",
- dbField, observerField)
- }
- endControlFlow()
-
- createRunQueryAndReturnStatements(builder = this,
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- dbField = dbField,
- inTransaction = inTransaction,
- scope = scope)
- }.build()
- }
-
- private fun createAnonymousObserver(): TypeSpec {
- val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
- return TypeSpec.anonymousClassBuilder(tableNamesList).apply {
- superclass(INVALIDATION_OBSERVER)
- addMethod(MethodSpec.methodBuilder("onInvalidated").apply {
- returns(TypeName.VOID)
- addAnnotation(Override::class.java)
- addParameter(ParameterSpec.builder(
- ParameterizedTypeName.get(Set::class.java, String::class.java), "tables")
- .addAnnotation(NonNull::class.java)
- .build())
- addModifiers(Modifier.PUBLIC)
- addStatement("invalidate()")
- }.build())
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/OptionalQueryResultAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/OptionalQueryResultAdapter.kt
deleted file mode 100644
index a092eee..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/OptionalQueryResultAdapter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2018 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.solver.query.result
-
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.ParameterizedTypeName
-
-/**
- * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
- * {@link java.util.Optional}.
- *
- * <p>n.b. this will only be useful if the project uses Java 8.
- */
-class OptionalQueryResultAdapter(private val resultAdapter: SingleEntityQueryResultAdapter)
- : QueryResultAdapter(resultAdapter.rowAdapter) {
- val type = resultAdapter.rowAdapter?.out
- override fun convert(
- outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- val valueVarName = scope.getTmpVar("_value")
- resultAdapter?.convert(valueVarName, cursorVarName, scope)
- addStatement(
- "final $T $L = $T.ofNullable($L)",
- ParameterizedTypeName.get(CommonTypeNames.OPTIONAL, type?.typeName()),
- outVarName,
- CommonTypeNames.OPTIONAL,
- valueVarName)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt
deleted file mode 100644
index ee6e88b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PojoRowAdapter.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.verifier.QueryResultInfo
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.FieldWithIndex
-import android.arch.persistence.room.vo.Pojo
-import android.arch.persistence.room.vo.RelationCollector
-import android.arch.persistence.room.vo.Warning
-import android.arch.persistence.room.writer.FieldReadWriteWriter
-import com.squareup.javapoet.TypeName
-import stripNonJava
-import javax.lang.model.type.TypeMirror
-
-/**
- * Creates the entity from the given info.
- * <p>
- * The info comes from the query processor so we know about the order of columns in the result etc.
- */
-class PojoRowAdapter(
- context: Context, private val info: QueryResultInfo?,
- val pojo: Pojo, out: TypeMirror) : RowAdapter(out) {
- val mapping: Mapping
- val relationCollectors: List<RelationCollector>
-
- init {
- // toMutableList documentation is not clear if it copies so lets be safe.
- val remainingFields = pojo.fields.mapTo(mutableListOf(), { it })
- val unusedColumns = arrayListOf<String>()
- val matchedFields: List<Field>
- if (info != null) {
- matchedFields = info.columns.map { column ->
- // first check remaining, otherwise check any. maybe developer wants to map the same
- // column into 2 fields. (if they want to post process etc)
- val field = remainingFields.firstOrNull { it.columnName == column.name } ?:
- pojo.fields.firstOrNull { it.columnName == column.name }
- if (field == null) {
- unusedColumns.add(column.name)
- null
- } else {
- remainingFields.remove(field)
- field
- }
- }.filterNotNull()
- if (unusedColumns.isNotEmpty() || remainingFields.isNotEmpty()) {
- val warningMsg = ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = pojo.typeName,
- unusedColumns = unusedColumns,
- allColumns = info.columns.map { it.name },
- unusedFields = remainingFields,
- allFields = pojo.fields
- )
- context.logger.w(Warning.CURSOR_MISMATCH, null, warningMsg)
- }
- val nonNulls = remainingFields.filter { it.nonNull }
- if (nonNulls.isNotEmpty()) {
- context.logger.e(ProcessorErrors.pojoMissingNonNull(
- pojoTypeName = pojo.typeName,
- missingPojoFields = nonNulls.map { it.name },
- allQueryColumns = info.columns.map { it.name }))
- }
- if (matchedFields.isEmpty()) {
- context.logger.e(ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
- }
- } else {
- matchedFields = remainingFields.map { it }
- remainingFields.clear()
- }
- relationCollectors = RelationCollector.createCollectors(context, pojo.relations)
-
- mapping = Mapping(
- matchedFields = matchedFields,
- unusedColumns = unusedColumns,
- unusedFields = remainingFields
- )
- }
-
- fun relationTableNames(): List<String> {
- return relationCollectors.flatMap {
- val queryTableNames = it.loadAllQuery.tables.map { it.name }
- if (it.rowAdapter is PojoRowAdapter) {
- it.rowAdapter.relationTableNames() + queryTableNames
- } else {
- queryTableNames
- }
- }.distinct()
- }
-
- override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
- relationCollectors.forEach { it.writeInitCode(scope) }
- mapping.fieldsWithIndices = mapping.matchedFields.map {
- val indexVar = scope.getTmpVar("_cursorIndexOf${it.name.stripNonJava().capitalize()}")
- val indexMethod = if (info == null) {
- "getColumnIndex"
- } else {
- "getColumnIndexOrThrow"
- }
- scope.builder().addStatement("final $T $L = $L.$L($S)",
- TypeName.INT, indexVar, cursorVarName, indexMethod, it.columnName)
- FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = info != null)
- }
- }
-
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- FieldReadWriteWriter.readFromCursor(
- outVar = outVarName,
- outPojo = pojo,
- cursorVar = cursorVarName,
- fieldsWithIndices = mapping.fieldsWithIndices,
- relationCollectors = relationCollectors,
- scope = scope)
- }
- }
-
- override fun onCursorFinished(): ((CodeGenScope) -> Unit)? =
- if (relationCollectors.isEmpty()) {
- // it is important to return empty to notify that we don't need any post process
- // task
- null
- } else {
- { scope ->
- relationCollectors.forEach { collector ->
- collector.writeCollectionCode(scope)
- }
- }
- }
-
- data class Mapping(
- val matchedFields: List<Field>,
- val unusedColumns: List<String>,
- val unusedFields: List<Field>) {
- // set when cursor is ready.
- lateinit var fieldsWithIndices: List<FieldWithIndex>
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
deleted file mode 100644
index 52f2621..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-
-class PositionalDataSourceQueryResultBinder(
- val listAdapter: ListQueryResultAdapter?,
- val tableNames: Set<String>) : QueryResultBinder(listAdapter) {
- val itemTypeName: TypeName = listAdapter?.rowAdapter?.out?.typeName() ?: TypeName.OBJECT
- val typeName: ParameterizedTypeName = ParameterizedTypeName.get(
- RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE, itemTypeName)
- override fun convertAndReturn(roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- // first comma for table names comes from the string since it might be empty in which case
- // we don't need a comma. If list is empty, this prevents generating bad code (it is still
- // an error to have empty list but that is already reported while item is processed)
- val tableNamesList = tableNames.joinToString("") { ", \"$it\"" }
- val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L $L",
- dbField, roomSQLiteQueryVar, inTransaction, tableNamesList).apply {
- superclass(typeName)
- addMethod(createConvertRowsMethod(scope))
- }.build()
- scope.builder().apply {
- addStatement("return $L", spec)
- }
- }
-
- private fun createConvertRowsMethod(scope: CodeGenScope): MethodSpec =
- MethodSpec.methodBuilder("convertRows").apply {
- addAnnotation(Override::class.java)
- addModifiers(Modifier.PROTECTED)
- returns(ParameterizedTypeName.get(CommonTypeNames.LIST, itemTypeName))
- val cursorParam = ParameterSpec.builder(AndroidTypeNames.CURSOR, "cursor")
- .build()
- addParameter(cursorParam)
- val resultVar = scope.getTmpVar("_res")
- val rowsScope = scope.fork()
- listAdapter?.convert(resultVar, cursorParam.name, rowsScope)
- addCode(rowsScope.builder().build())
- addStatement("return $L", resultVar)
- }.build()
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultAdapter.kt
deleted file mode 100644
index 2f6ca75..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultAdapter.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.solver.CodeGenScope
-
-/**
- * Gets a Cursor and converts it into the return type of a method annotated with @Query.
- */
-abstract class QueryResultAdapter(val rowAdapter: RowAdapter?) {
- abstract fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope)
- fun accessedTableNames(): List<String> {
- return (rowAdapter as? PojoRowAdapter)?.relationTableNames() ?: emptyList<String>()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
deleted file mode 100644
index 9fff325..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.FieldSpec
-
-/**
- * Connects the query, db and the ResultAdapter.
- * <p>
- * The default implementation is InstantResultBinder. If the query is deferred rather than executed
- * directly, such alternative implementations can be implement using this interface (e.g. LiveData,
- * Rx, caching etc)
- */
-abstract class QueryResultBinder(val adapter: QueryResultAdapter?) {
- /**
- * receives the sql, bind args and adapter and generates the code that runs the query
- * and returns the result.
- */
- abstract fun convertAndReturn(
- roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean, // false if query is provided by the user
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RowAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RowAdapter.kt
deleted file mode 100644
index cb25d97..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RowAdapter.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.lang.model.type.TypeMirror
-
-/**
- * Converts a row of a cursor result into an Entity or a primitive.
- * <p>
- * An instance of this is created for each usage so that it can keep local variables.
- */
-abstract class RowAdapter(val out: TypeMirror) {
- /**
- * Called when cursor variable is ready, good place to put initialization code.
- */
- open fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {}
-
- /**
- * Called to convert a single row.
- */
- abstract fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope)
-
- /**
- * Called when the cursor is finished. It is important to return null if no operation is
- * necessary so that caller can understand that we can do lazy loading.
- */
- open fun onCursorFinished(): ((scope: CodeGenScope) -> Unit)? = null
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
deleted file mode 100644
index a91773d..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomRxJava2TypeNames
-import android.arch.persistence.room.ext.RxJava2TypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-import javax.lang.model.type.TypeMirror
-
-/**
- * Generic Result binder for Rx classes that accept a callable.
- */
-class RxCallableQueryResultBinder(val rxType: RxType,
- val typeArg: TypeMirror,
- adapter: QueryResultAdapter?)
- : QueryResultBinder(adapter) {
- override fun convertAndReturn(roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope) {
- val callable = TypeSpec.anonymousClassBuilder("").apply {
- val typeName = typeArg.typeName()
- superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
- typeName))
- addMethod(createCallMethod(
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- canReleaseQuery = canReleaseQuery,
- dbField = dbField,
- inTransaction = inTransaction,
- scope = scope))
- }.build()
- scope.builder().apply {
- addStatement("return $T.fromCallable($L)", rxType.className, callable)
- }
- }
-
- private fun createCallMethod(roomSQLiteQueryVar: String,
- canReleaseQuery: Boolean,
- dbField: FieldSpec,
- inTransaction: Boolean,
- scope: CodeGenScope): MethodSpec {
- val adapterScope = scope.fork()
- return MethodSpec.methodBuilder("call").apply {
- returns(typeArg.typeName())
- addException(Exception::class.typeName())
- addModifiers(Modifier.PUBLIC)
- addAnnotation(Override::class.java)
- val transactionWrapper = if (inTransaction) {
- transactionWrapper(dbField)
- } else {
- null
- }
- transactionWrapper?.beginTransactionWithControlFlow()
- val outVar = scope.getTmpVar("_result")
- val cursorVar = scope.getTmpVar("_cursor")
- addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
- dbField, roomSQLiteQueryVar)
- beginControlFlow("try").apply {
- adapter?.convert(outVar, cursorVar, adapterScope)
- addCode(adapterScope.generate())
- if (!rxType.canBeNull) {
- beginControlFlow("if($L == null)", outVar).apply {
- addStatement("throw new $T($S + $L.getSql())",
- RoomRxJava2TypeNames.RX_EMPTY_RESULT_SET_EXCEPTION,
- "Query returned empty result set: ",
- roomSQLiteQueryVar)
- }
- endControlFlow()
- }
- transactionWrapper?.commitTransaction()
- addStatement("return $L", outVar)
- }
- nextControlFlow("finally").apply {
- addStatement("$L.close()", cursorVar)
- if (canReleaseQuery) {
- addStatement("$L.release()", roomSQLiteQueryVar)
- }
- }
- endControlFlow()
- transactionWrapper?.endTransactionWithControlFlow()
- }.build()
- }
-
- enum class RxType(val className: ClassName, val canBeNull: Boolean) {
- SINGLE(RxJava2TypeNames.SINGLE, canBeNull = false),
- MAYBE(RxJava2TypeNames.MAYBE, canBeNull = true);
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/SingleColumnRowAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/SingleColumnRowAdapter.kt
deleted file mode 100644
index 8237177..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/SingleColumnRowAdapter.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.solver.types.CursorValueReader
-
-/**
- * Wraps a row adapter when there is only 1 item with 1 column in the response.
- */
-class SingleColumnRowAdapter(val reader: CursorValueReader) : RowAdapter(reader.typeMirror()) {
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- reader.readFromCursor(outVarName, cursorVarName, "0", scope)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/SingleEntityQueryResultAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/SingleEntityQueryResultAdapter.kt
deleted file mode 100644
index 3086894..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/SingleEntityQueryResultAdapter.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query.result
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import defaultValue
-
-/**
- * Wraps a row adapter when there is only 1 item in the result
- */
-class SingleEntityQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
- val type = rowAdapter.out
- override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- rowAdapter?.onCursorReady(cursorVarName, scope)
- addStatement("final $T $L", type.typeName(), outVarName)
- beginControlFlow("if($L.moveToFirst())", cursorVarName)
- rowAdapter?.convert(outVarName, cursorVarName, scope)
- nextControlFlow("else").apply {
- addStatement("$L = $L", outVarName, rowAdapter?.out?.defaultValue())
- }
- endControlFlow()
- rowAdapter?.onCursorFinished()?.invoke(scope)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
deleted file mode 100644
index b48f179..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.solver.query.result
-
-import android.arch.persistence.room.ext.N
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-
-/**
- * helper class to create correct transaction code.
- */
-interface TransactionWrapper {
- fun beginTransactionWithControlFlow()
- fun commitTransaction()
- fun endTransactionWithControlFlow()
-}
-
-fun MethodSpec.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
- override fun beginTransactionWithControlFlow() {
- addStatement("$N.beginTransaction()", dbField)
- beginControlFlow("try")
- }
-
- override fun commitTransaction() {
- addStatement("$N.setTransactionSuccessful()", dbField)
- }
-
- override fun endTransactionWithControlFlow() {
- nextControlFlow("finally")
- addStatement("$N.endTransaction()", dbField)
- endControlFlow()
- }
-}
-
-fun CodeBlock.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
- override fun beginTransactionWithControlFlow() {
- addStatement("$N.beginTransaction()", dbField)
- beginControlFlow("try")
- }
-
- override fun commitTransaction() {
- addStatement("$N.setTransactionSuccessful()", dbField)
- }
-
- override fun endTransactionWithControlFlow() {
- nextControlFlow("finally")
- addStatement("$N.endTransaction()", dbField)
- endControlFlow()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/BoxedBooleanToBoxedIntConverter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
deleted file mode 100644
index 70030a8..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-
-/**
- * int to boolean adapter.
- */
-object BoxedBooleanToBoxedIntConverter {
- fun create(processingEnvironment: ProcessingEnvironment): List<TypeConverter> {
- val tBoolean = processingEnvironment.elementUtils.getTypeElement("java.lang.Boolean")
- .asType()
- val tInt = processingEnvironment.elementUtils.getTypeElement("java.lang.Integer")
- .asType()
- return listOf(
- object : TypeConverter(tBoolean, tInt) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().addStatement("$L = $L == null ? null : ($L ? 1 : 0)",
- outputVarName, inputVarName, inputVarName)
- }
- },
- object : TypeConverter(tInt, tBoolean) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().addStatement("$L = $L == null ? null : $L != 0",
- outputVarName, inputVarName, inputVarName)
- }
- }
- )
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
deleted file mode 100644
index debc59a..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.solver.CodeGenScope
-import com.google.auto.common.MoreTypes
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.TypeMirror
-
-/**
- * Adapters for all boxed primitives that has direct cursor mappings.
- */
-open class BoxedPrimitiveColumnTypeAdapter(
- boxed: TypeMirror,
- val primitiveAdapter: PrimitiveColumnTypeAdapter
-) : ColumnTypeAdapter(boxed, primitiveAdapter.typeAffinity) {
- companion object {
- fun createBoxedPrimitiveAdapters(
- processingEnvironment: ProcessingEnvironment,
- primitiveAdapters: List<PrimitiveColumnTypeAdapter>
- ): List<ColumnTypeAdapter> {
-
- return primitiveAdapters.map {
- BoxedPrimitiveColumnTypeAdapter(
- processingEnvironment.typeUtils
- .boxedClass(MoreTypes.asPrimitiveType(it.out)).asType(),
- it
- )
- }
- }
- }
-
- override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- beginControlFlow("if ($L == null)", valueVarName).apply {
- addStatement("$L.bindNull($L)", stmtName, indexVarName)
- }
- nextControlFlow("else").apply {
- primitiveAdapter.bindToStmt(stmtName, indexVarName, valueVarName, scope)
- }
- endControlFlow()
- }
- }
-
- override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- beginControlFlow("if ($L.isNull($L))", cursorVarName, indexVarName).apply {
- addStatement("$L = null", outVarName)
- }
- nextControlFlow("else").apply {
- primitiveAdapter.readFromCursor(outVarName, cursorVarName, indexVarName, scope)
- }
- endControlFlow()
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/ByteArrayColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/ByteArrayColumnTypeAdapter.kt
deleted file mode 100644
index 5de76de..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/ByteArrayColumnTypeAdapter.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.TypeKind
-
-class ByteArrayColumnTypeAdapter(env: ProcessingEnvironment) : ColumnTypeAdapter(
- out = env.typeUtils.getArrayType(env.typeUtils.getPrimitiveType(TypeKind.BYTE)),
- typeAffinity = SQLTypeAffinity.BLOB) {
- override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
- scope: CodeGenScope) {
- scope.builder()
- .addStatement("$L = $L.getBlob($L)", outVarName, cursorVarName, indexVarName)
- }
-
- override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- beginControlFlow("if ($L == null)", valueVarName)
- .addStatement("$L.bindNull($L)", stmtName, indexVarName)
- nextControlFlow("else")
- .addStatement("$L.bindBlob($L, $L)", stmtName, indexVarName, valueVarName)
- endControlFlow()
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/ColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/ColumnTypeAdapter.kt
deleted file mode 100644
index d82dc79..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/ColumnTypeAdapter.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import com.squareup.javapoet.TypeName
-import javax.lang.model.type.TypeMirror
-
-/**
- * A code generator that can read a field from Cursor and write a field to a Statement
- */
-abstract class ColumnTypeAdapter(val out: TypeMirror, val typeAffinity: SQLTypeAffinity) :
- StatementValueBinder, CursorValueReader {
- val outTypeName: TypeName by lazy { TypeName.get(out) }
- override fun typeMirror() = out
- override fun affinity(): SQLTypeAffinity = typeAffinity
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CompositeAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CompositeAdapter.kt
deleted file mode 100644
index a8678ff..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CompositeAdapter.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.lang.model.type.TypeMirror
-
-/**
- * A column adapter that uses a type converter to do the conversion. The type converter may be
- * a composite one.
- */
-class CompositeAdapter(out: TypeMirror, val columnTypeAdapter: ColumnTypeAdapter,
- val intoStatementConverter: TypeConverter?,
- val fromCursorConverter: TypeConverter?)
- : ColumnTypeAdapter(out, columnTypeAdapter.typeAffinity) {
- override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
- scope: CodeGenScope) {
- if (fromCursorConverter == null) {
- return
- }
- scope.builder().apply {
- val tmpCursorValue = scope.getTmpVar()
- addStatement("final $T $L", columnTypeAdapter.outTypeName, tmpCursorValue)
- columnTypeAdapter.readFromCursor(tmpCursorValue, cursorVarName, indexVarName, scope)
- fromCursorConverter.convert(tmpCursorValue, outVarName, scope)
- }
- }
-
- override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
- scope: CodeGenScope) {
- if (intoStatementConverter == null) {
- return
- }
- scope.builder().apply {
- val tmpVar = scope.getTmpVar()
- addStatement("final $T $L", columnTypeAdapter.out, tmpVar)
- intoStatementConverter.convert(valueVarName, tmpVar, scope)
- columnTypeAdapter.bindToStmt(stmtName, indexVarName, tmpVar, scope)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CompositeTypeConverter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CompositeTypeConverter.kt
deleted file mode 100644
index b02709a..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CompositeTypeConverter.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-
-/**
- * combines 2 type converters
- */
-class CompositeTypeConverter(val conv1: TypeConverter, val conv2: TypeConverter) : TypeConverter(
- conv1.from, conv2.to) {
- override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- val tmp = scope.getTmpVar()
- addStatement("final $T $L", conv1.to.typeName(), tmp)
- conv1.convert(inputVarName, tmp, scope)
- conv2.convert(tmp, outputVarName, scope)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CursorValueReader.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CursorValueReader.kt
deleted file mode 100644
index 9293143..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CursorValueReader.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.solver.types
-
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.lang.model.type.TypeMirror
-
-/**
- * Reads value from a cursor at the given index.
- * see: StatementValueBinder
- */
-interface CursorValueReader {
- fun affinity(): SQLTypeAffinity
- fun typeMirror(): TypeMirror
- fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
- scope: CodeGenScope)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CustomTypeConverterWrapper.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CustomTypeConverterWrapper.kt
deleted file mode 100644
index c127203..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/CustomTypeConverterWrapper.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.CustomTypeConverter
-import android.arch.persistence.room.writer.ClassWriter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import javax.lang.model.element.Modifier
-
-/**
- * Wraps a type converter specified by the developer and forwards calls to it.
- */
-class CustomTypeConverterWrapper(val custom: CustomTypeConverter)
- : TypeConverter(custom.from, custom.to) {
-
- override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
- if (custom.isStatic) {
- addStatement("$L = $T.$L($L)",
- outputVarName, custom.typeName,
- custom.methodName, inputVarName)
- } else {
- addStatement("$L = $N.$L($L)",
- outputVarName, typeConverter(scope),
- custom.methodName, inputVarName)
- }
- }
- }
-
- fun typeConverter(scope: CodeGenScope): FieldSpec {
- val baseName = (custom.typeName as ClassName).simpleName().decapitalize()
- return scope.writer.getOrCreateField(object : ClassWriter.SharedFieldSpec(
- baseName, custom.typeName) {
- override fun getUniqueKey(): String {
- return "converter_${custom.typeName}"
- }
-
- override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
- builder.addModifiers(Modifier.PRIVATE)
- builder.addModifiers(Modifier.FINAL)
- builder.initializer("new $T()", custom.typeName)
- }
- })
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/NoOpConverter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/NoOpConverter.kt
deleted file mode 100644
index c37698b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/NoOpConverter.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.lang.model.type.TypeMirror
-
-/**
- * Yes, we need this when user input is the same as the desired output.
- * <p>
- * Each query parameter receives an adapter that converts it into a String (or String[]). This
- * TypeAdapter basically serves as a wrapper for converting String parameter into the String[] of
- * the query. Not having this would require us to special case handle String, String[], List<String>
- * etc.
- */
-class NoOpConverter(type: TypeMirror) : TypeConverter(
- type, type) {
- override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
- scope.builder()
- .addStatement("$L = $L", outputVarName, inputVarName)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/PrimitiveBooleanToIntConverter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/PrimitiveBooleanToIntConverter.kt
deleted file mode 100644
index c65df5c..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/PrimitiveBooleanToIntConverter.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.TypeKind.BOOLEAN
-import javax.lang.model.type.TypeKind.INT
-
-/**
- * int to boolean adapter.
- */
-object PrimitiveBooleanToIntConverter {
- fun create(processingEnvironment: ProcessingEnvironment): List<TypeConverter> {
- val tBoolean = processingEnvironment.typeUtils.getPrimitiveType(BOOLEAN)
- val tInt = processingEnvironment.typeUtils.getPrimitiveType(INT)
- return listOf(
- object : TypeConverter(tBoolean, tInt) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().addStatement("$L = $L ? 1 : 0", outputVarName, inputVarName)
- }
- },
- object : TypeConverter(tInt, tBoolean) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().addStatement("$L = $L != 0", outputVarName, inputVarName)
- }
- })
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/PrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/PrimitiveColumnTypeAdapter.kt
deleted file mode 100644
index 69cc0f8..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/PrimitiveColumnTypeAdapter.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.parser.SQLTypeAffinity.REAL
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.PrimitiveType
-import javax.lang.model.type.TypeKind.BYTE
-import javax.lang.model.type.TypeKind.CHAR
-import javax.lang.model.type.TypeKind.DOUBLE
-import javax.lang.model.type.TypeKind.FLOAT
-import javax.lang.model.type.TypeKind.INT
-import javax.lang.model.type.TypeKind.LONG
-import javax.lang.model.type.TypeKind.SHORT
-
-/**
- * Adapters for all primitives that has direct cursor mappings.
- */
-open class PrimitiveColumnTypeAdapter(out: PrimitiveType,
- val cursorGetter: String,
- val stmtSetter: String,
- typeAffinity: SQLTypeAffinity)
- : ColumnTypeAdapter(out, typeAffinity) {
- val cast = if (cursorGetter == "get${out.typeName().toString().capitalize()}")
- ""
- else
- "(${out.typeName()}) "
-
- companion object {
- fun createPrimitiveAdapters(
- processingEnvironment: ProcessingEnvironment
- ): List<PrimitiveColumnTypeAdapter> {
- return listOf(
- Triple(INT, "getInt", "bindLong"),
- Triple(SHORT, "getShort", "bindLong"),
- Triple(BYTE, "getShort", "bindLong"),
- Triple(LONG, "getLong", "bindLong"),
- Triple(CHAR, "getInt", "bindLong"),
- Triple(FLOAT, "getFloat", "bindDouble"),
- Triple(DOUBLE, "getDouble", "bindDouble")
- ).map {
- PrimitiveColumnTypeAdapter(
- out = processingEnvironment.typeUtils.getPrimitiveType(it.first),
- cursorGetter = it.second,
- stmtSetter = it.third,
- typeAffinity = when (it.first) {
- INT, SHORT, BYTE, LONG, CHAR -> SQLTypeAffinity.INTEGER
- FLOAT, DOUBLE -> REAL
- else -> throw IllegalArgumentException("invalid type")
- }
- )
- }
- }
- }
-
- override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
- scope: CodeGenScope) {
- scope.builder()
- .addStatement("$L.$L($L, $L)", stmtName, stmtSetter, indexVarName, valueVarName)
- }
-
- override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
- scope: CodeGenScope) {
- scope.builder()
- .addStatement("$L = $L$L.$L($L)", outVarName, cast, cursorVarName,
- cursorGetter, indexVarName)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/StatementValueBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/StatementValueBinder.kt
deleted file mode 100644
index d83c145..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/StatementValueBinder.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.solver.types
-
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.lang.model.type.TypeMirror
-
-/**
- * Binds a value into a statement
- * see: CursorValueReader
- */
-interface StatementValueBinder {
- fun typeMirror(): TypeMirror
- fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
- scope: CodeGenScope)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/StringColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/StringColumnTypeAdapter.kt
deleted file mode 100644
index 34ca61f..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/StringColumnTypeAdapter.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.parser.SQLTypeAffinity.TEXT
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-
-class StringColumnTypeAdapter(processingEnvironment: ProcessingEnvironment)
- : ColumnTypeAdapter((processingEnvironment.elementUtils.getTypeElement(
- String::class.java.canonicalName)).asType(), TEXT) {
- override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
- scope: CodeGenScope) {
- scope.builder()
- .addStatement("$L = $L.getString($L)", outVarName, cursorVarName, indexVarName)
- }
-
- override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- beginControlFlow("if ($L == null)", valueVarName)
- .addStatement("$L.bindNull($L)", stmtName, indexVarName)
- nextControlFlow("else")
- .addStatement("$L.bindString($L, $L)", stmtName, indexVarName, valueVarName)
- endControlFlow()
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/TypeConverter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/TypeConverter.kt
deleted file mode 100644
index 67b70dc..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/types/TypeConverter.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.types
-
-import android.arch.persistence.room.solver.CodeGenScope
-import javax.lang.model.type.TypeMirror
-
-/**
- * A code generator that can convert from 1 type to another
- */
-abstract class TypeConverter(val from: TypeMirror, val to: TypeMirror) {
- abstract fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope)
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/ColumnInfo.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/ColumnInfo.kt
deleted file mode 100644
index 652860b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/ColumnInfo.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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.verifier
-
-import android.arch.persistence.room.parser.SQLTypeAffinity
-
-/**
- * Represents a column in a query response
- */
-data class ColumnInfo(val name: String, val type: SQLTypeAffinity)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/DatabaseVerificaitonErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/DatabaseVerificaitonErrors.kt
deleted file mode 100644
index 6b133b4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/DatabaseVerificaitonErrors.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.verifier
-
-import java.sql.SQLException
-
-object DatabaseVerificaitonErrors {
- private val CANNOT_CREATE_TABLE: String = "Create table statement had an error: %s"
- fun cannotCreateTable(exception: SQLException): String {
- return CANNOT_CREATE_TABLE.format(exception.message)
- }
-
- private val CANNOT_VERIFY_QUERY: String = "There is a problem with the query: %s"
- fun cannotVerifyQuery(exception: SQLException): String {
- return CANNOT_VERIFY_QUERY.format(exception.message)
- }
-
- private val CANNOT_CREATE_SQLITE_CONNECTION: String = "Room cannot create an SQLite" +
- " connection to verify the queries. Query verification will be disabled. Error: %s"
- fun cannotCreateConnection(exception: Exception): String {
- return CANNOT_CREATE_SQLITE_CONNECTION.format(exception.message)
- }
-
- val CANNOT_GET_TMP_JAVA_DIR = "Cannot read tmp java dir which is necessary to load sqlite" +
- " lib. Database SQL verification will be disabled"
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/DatabaseVerifier.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/DatabaseVerifier.kt
deleted file mode 100644
index 4e1c639..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/DatabaseVerifier.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.verifier
-
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.Warning
-import columnInfo
-import org.sqlite.JDBC
-import java.io.File
-import java.sql.Connection
-import java.sql.DriverManager
-import java.sql.SQLException
-import java.util.UUID
-import java.util.regex.Pattern
-import javax.lang.model.element.Element
-
-/**
- * Builds an in-memory version of the database and verifies the queries against it.
- * This class is also used to resolve the return types.
- */
-class DatabaseVerifier private constructor(
- val connection: Connection, val context: Context, val entities: List<Entity>) {
- companion object {
- private const val CONNECTION_URL = "jdbc:sqlite::memory:"
- /**
- * Taken from:
- * https://github.com/robolectric/robolectric/blob/master/shadows/framework/
- * src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java#L94
- *
- * This is actually not accurate because it might swap anything since it does not parse
- * SQL. That being said, for the verification purposes, it does not matter and clearly
- * much easier than parsing and rebuilding the query.
- */
- private val COLLATE_LOCALIZED_UNICODE_PATTERN = Pattern.compile(
- "\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE)
-
- init {
- // see: https://github.com/xerial/sqlite-jdbc/issues/97
- val tmpDir = System.getProperty("java.io.tmpdir")
- if (tmpDir != null) {
- val outDir = File(tmpDir, "room-${UUID.randomUUID()}")
- outDir.mkdirs()
- outDir.deleteOnExit()
- System.setProperty("org.sqlite.tmpdir", outDir.absolutePath)
- // dummy call to trigger JDBC initialization so that we can unregister it
- JDBC.isValidURL(CONNECTION_URL)
- unregisterDrivers()
- }
- }
-
- /**
- * Tries to create a verifier but returns null if it cannot find the driver.
- */
- fun create(context: Context, element: Element, entities: List<Entity>): DatabaseVerifier? {
- return try {
- val connection = JDBC.createConnection(CONNECTION_URL, java.util.Properties())
- DatabaseVerifier(connection, context, entities)
- } catch (ex: Exception) {
- context.logger.w(Warning.CANNOT_CREATE_VERIFICATION_DATABASE, element,
- DatabaseVerificaitonErrors.cannotCreateConnection(ex))
- null
- }
- }
-
- /**
- * Unregisters the JDBC driver. If we don't do this, we'll leak the driver which leaks a
- * whole class loader.
- * see: https://github.com/xerial/sqlite-jdbc/issues/267
- * see: https://issuetracker.google.com/issues/62473121
- */
- private fun unregisterDrivers() {
- try {
- DriverManager.getDriver(CONNECTION_URL)?.let {
- DriverManager.deregisterDriver(it)
- }
- } catch (t: Throwable) {
- System.err.println("Room: cannot unregister driver ${t.message}")
- }
- }
- }
- init {
- entities.forEach { entity ->
- val stmt = connection.createStatement()
- stmt.executeUpdate(stripLocalizeCollations(entity.createTableQuery))
- }
- }
-
- fun analyze(sql: String): QueryResultInfo {
- return try {
- val stmt = connection.prepareStatement(stripLocalizeCollations(sql))
- QueryResultInfo(stmt.columnInfo())
- } catch (ex: SQLException) {
- QueryResultInfo(emptyList(), ex)
- }
- }
-
- private fun stripLocalizeCollations(sql: String) =
- COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql).replaceAll(" COLLATE NOCASE")
-
- fun closeConnection(context: Context) {
- if (!connection.isClosed) {
- try {
- connection.close()
- } catch (t: Throwable) {
- //ignore.
- context.logger.d("failed to close the database connection ${t.message}")
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/QueryResultInfo.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/QueryResultInfo.kt
deleted file mode 100644
index 41b348f..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/QueryResultInfo.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.verifier
-
-import java.sql.SQLException
-
-/**
- * Represents the result of a query.
- * <p>
- * This information is obtained by preparing the query against an in memory database at compile
- * time.
- */
-data class QueryResultInfo(val columns: List<ColumnInfo>, val error: SQLException? = null)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/jdbc_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/jdbc_ext.kt
deleted file mode 100644
index 47b9442..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/verifier/jdbc_ext.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.
- */
-
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.verifier.ColumnInfo
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.sql.ResultSetMetaData
-import java.sql.SQLException
-
-internal fun <T> ResultSet.collect(f: (ResultSet) -> T): List<T> {
- val result = arrayListOf<T>()
- try {
- while (next()) {
- result.add(f.invoke(this))
- }
- } finally {
- close()
- }
- return result
-}
-
-private fun <T> PreparedStatement.map(f: (Int, ResultSetMetaData) -> T): List<T> {
- val columnCount = try {
- metaData.columnCount
- } catch (ex: SQLException) {
- // ignore, no-result query
- 0
- }
- // return is separate than data creation because we want to know who throws the exception
- return (1.rangeTo(columnCount)).map { f(it, metaData) }
-}
-
-internal fun PreparedStatement.columnNames(): List<String> {
- return map { index, data -> data.getColumnName(index) }
-}
-
-private fun PreparedStatement.tryGetAffinity(columnIndex: Int): SQLTypeAffinity {
- return try {
- SQLTypeAffinity.valueOf(metaData.getColumnTypeName(columnIndex).capitalize())
- } catch (ex: IllegalArgumentException) {
- SQLTypeAffinity.NULL
- }
-}
-
-internal fun PreparedStatement.columnInfo(): List<ColumnInfo> {
- //see: http://sqlite.1065341.n5.nabble.com/Column-order-in-resultset-td23127.html
- return map { index, data -> ColumnInfo(data.getColumnName(index), tryGetAffinity(index)) }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/CallType.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/CallType.kt
deleted file mode 100644
index 40cbcc4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/CallType.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-enum class CallType {
- FIELD,
- METHOD,
- CONSTRUCTOR
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Constructor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Constructor.kt
deleted file mode 100644
index 112c9b4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Constructor.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.vo
-
-import javax.lang.model.element.ExecutableElement
-
-/**
- * For each Entity / Pojo we process has a constructor. It might be the empty constructor or a
- * constructor with fields.
- */
-data class Constructor(val element: ExecutableElement, val params: List<Param>) {
-
- fun hasField(field: Field): Boolean {
- return params.any {
- when (it) {
- is FieldParam -> it.field === field
- is EmbeddedParam -> it.embedded.field === field
- else -> false
- }
- }
- }
-
- class FieldParam(val field: Field) : Param(ParamType.FIELD) {
- override fun log(): String = field.getPath()
- }
-
- class EmbeddedParam(val embedded: EmbeddedField) : Param(ParamType.EMBEDDED) {
- override fun log(): String = embedded.field.getPath()
- }
-
- abstract class Param(val type: ParamType) {
- abstract fun log(): String
- }
-
- enum class ParamType {
- FIELD,
- EMBEDDED
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/CustomTypeConverter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/CustomTypeConverter.kt
deleted file mode 100644
index 3bbae96..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/CustomTypeConverter.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.ext.hasAnyOf
-import android.arch.persistence.room.ext.typeName
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier
-import javax.lang.model.type.TypeMirror
-
-/**
- * Generated when we parse a method annotated with TypeConverter.
- */
-data class CustomTypeConverter(val type: TypeMirror,
- val method: ExecutableElement,
- val from: TypeMirror, val to: TypeMirror) {
- val typeName: TypeName by lazy { type.typeName() }
- val fromTypeName: TypeName by lazy { from.typeName() }
- val toTypeName: TypeName by lazy { to.typeName() }
- val methodName by lazy { method.simpleName.toString() }
- val isStatic by lazy { method.hasAnyOf(Modifier.STATIC) }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
deleted file mode 100644
index 774624c..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-
-data class Dao(
- val element: TypeElement, val type: DeclaredType,
- val queryMethods: List<QueryMethod>,
- val rawQueryMethods: List<RawQueryMethod>,
- val insertionMethods: List<InsertionMethod>,
- val deletionMethods: List<DeletionMethod>,
- val updateMethods: List<UpdateMethod>,
- val transactionMethods: List<TransactionMethod>,
- val constructorParamType: TypeName?) {
- // parsed dao might have a suffix if it is used in multiple databases.
- private var suffix: String? = null
-
- fun setSuffix(newSuffix: String) {
- if (this.suffix != null) {
- throw IllegalStateException("cannot set suffix twice")
- }
- this.suffix = if (newSuffix == "") "" else "_$newSuffix"
- }
-
- val typeName: ClassName by lazy { ClassName.get(element) }
-
- val shortcutMethods: List<ShortcutMethod> by lazy {
- deletionMethods + updateMethods
- }
-
- private val implClassName by lazy {
- if (suffix == null) {
- suffix = ""
- }
- val path = arrayListOf<String>()
- var enclosing = element.enclosingElement
- while (enclosing is TypeElement) {
- path.add(ClassName.get(enclosing as TypeElement).simpleName())
- enclosing = enclosing.enclosingElement
- }
- path.reversed().joinToString("_") + "${typeName.simpleName()}${suffix}_Impl"
- }
-
- val implTypeName: ClassName by lazy {
- ClassName.get(typeName.packageName(), implClassName)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/DaoMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/DaoMethod.kt
deleted file mode 100644
index bf134ad..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/DaoMethod.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import javax.lang.model.element.Element
-
-/**
- * References a method that returns a dao in a Database
- */
-data class DaoMethod(val element: Element, val name: String, val dao: Dao)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
deleted file mode 100644
index 2cffbf3..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Database.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.RoomMasterTable
-import android.arch.persistence.room.migration.bundle.DatabaseBundle
-import android.arch.persistence.room.migration.bundle.SchemaBundle
-import com.squareup.javapoet.ClassName
-import org.apache.commons.codec.digest.DigestUtils
-import java.io.File
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeMirror
-
-/**
- * Holds information about a class annotated with Database.
- */
-data class Database(val element: TypeElement,
- val type: TypeMirror,
- val entities: List<Entity>,
- val daoMethods: List<DaoMethod>,
- val version: Int,
- val exportSchema: Boolean,
- val enableForeignKeys: Boolean) {
- val typeName: ClassName by lazy { ClassName.get(element) }
-
- private val implClassName by lazy {
- "${typeName.simpleNames().joinToString("_")}_Impl"
- }
-
- val implTypeName: ClassName by lazy {
- ClassName.get(typeName.packageName(), implClassName)
- }
-
- val bundle by lazy {
- DatabaseBundle(version, identityHash, entities.map(Entity::toBundle),
- listOf(RoomMasterTable.CREATE_QUERY,
- RoomMasterTable.createInsertQuery(identityHash)))
- }
-
- /**
- * Create a has that identifies this database definition so that at runtime we can check to
- * ensure developer didn't forget to update the version.
- */
- val identityHash: String by lazy {
- val idKey = SchemaIdentityKey()
- idKey.appendSorted(entities)
- idKey.hash()
- }
-
- val legacyIdentityHash: String by lazy {
- val entityDescriptions = entities
- .sortedBy { it.tableName }
- .map { it.createTableQuery }
- val indexDescriptions = entities
- .flatMap { entity ->
- entity.indices.map { index ->
- index.createQuery(entity.tableName)
- }
- }
- val input = (entityDescriptions + indexDescriptions).joinToString("¯\\_(ツ)_/¯")
- DigestUtils.md5Hex(input)
- }
-
- fun exportSchema(file: File) {
- val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
- if (file.exists()) {
- val existing = file.inputStream().use {
- SchemaBundle.deserialize(it)
- }
- if (existing.isSchemaEqual(schemaBundle)) {
- return
- }
- }
- SchemaBundle.serialize(schemaBundle, file)
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/DeletionMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/DeletionMethod.kt
deleted file mode 100644
index e122c85..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/DeletionMethod.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.vo
-
-import javax.lang.model.element.ExecutableElement
-
-class DeletionMethod(
- element: ExecutableElement,
- name: String,
- entities: Map<String, Entity>, returnCount: Boolean,
- parameters: List<ShortcutQueryParameter>
-) : ShortcutMethod(element, name, entities, returnCount, parameters)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/EmbeddedField.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/EmbeddedField.kt
deleted file mode 100644
index 55539a2..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/EmbeddedField.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.vo
-
-import android.support.annotation.NonNull
-import android.arch.persistence.room.ext.hasAnnotation
-
-/**
- * Used when a field is embedded inside an Entity or Pojo.
- */
-// used in cache matching, must stay as a data class or implement equals
-data class EmbeddedField(val field: Field, val prefix: String = "",
- val parent: EmbeddedField?) {
- val getter by lazy { field.getter }
- val setter by lazy { field.setter }
- val nonNull = field.element.hasAnnotation(NonNull::class)
- lateinit var pojo: Pojo
- val mRootParent: EmbeddedField by lazy {
- parent?.mRootParent ?: this
- }
-
- fun isDescendantOf(other: EmbeddedField): Boolean {
- if (parent == other) {
- return true
- } else if (parent == null) {
- return false
- } else {
- return parent.isDescendantOf(other)
- }
- }
-
- fun isNonNullRecursively(): Boolean {
- return field.nonNull && (parent == null || parent.isNonNullRecursively())
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
deleted file mode 100644
index cf37bad..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Entity.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.migration.bundle.BundleUtil
-import android.arch.persistence.room.migration.bundle.EntityBundle
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-
-// TODO make data class when move to kotlin 1.1
-class Entity(
- element: TypeElement, val tableName: String, type: DeclaredType,
- fields: List<Field>, embeddedFields: List<EmbeddedField>,
- val primaryKey: PrimaryKey, val indices: List<Index>,
- val foreignKeys: List<ForeignKey>,
- constructor: Constructor?)
- : Pojo(element, type, fields, embeddedFields, emptyList(), constructor), HasSchemaIdentity {
-
- val createTableQuery by lazy {
- createTableQuery(tableName)
- }
-
- // a string defining the identity of this entity, which can be used for equality checks
- override fun getIdKey(): String {
- val identityKey = SchemaIdentityKey()
- identityKey.append(tableName)
- identityKey.append(primaryKey)
- identityKey.appendSorted(fields)
- identityKey.appendSorted(indices)
- identityKey.appendSorted(foreignKeys)
- return identityKey.hash()
- }
-
- private fun createTableQuery(tableName: String): String {
- val definitions = (fields.map {
- val autoIncrement = primaryKey.autoGenerateId && primaryKey.fields.contains(it)
- it.databaseDefinition(autoIncrement)
- } + createPrimaryKeyDefinition() + createForeignKeyDefinitions()).filterNotNull()
- return "CREATE TABLE IF NOT EXISTS `$tableName` (${definitions.joinToString(", ")})"
- }
-
- private fun createForeignKeyDefinitions(): List<String> {
- return foreignKeys.map { it.databaseDefinition() }
- }
-
- private fun createPrimaryKeyDefinition(): String? {
- return if (primaryKey.fields.isEmpty() || primaryKey.autoGenerateId) {
- null
- } else {
- val keys = primaryKey.fields
- .map { "`${it.columnName}`" }
- .joinToString(", ")
- "PRIMARY KEY($keys)"
- }
- }
-
- fun shouldBeDeletedAfter(other: Entity): Boolean {
- return foreignKeys.any {
- it.parentTable == other.tableName
- && ((!it.deferred && it.onDelete == ForeignKeyAction.NO_ACTION)
- || it.onDelete == ForeignKeyAction.RESTRICT)
- }
- }
-
- fun toBundle(): EntityBundle = EntityBundle(
- tableName,
- createTableQuery(BundleUtil.TABLE_NAME_PLACEHOLDER),
- fields.map { it.toBundle() },
- primaryKey.toBundle(),
- indices.map { it.toBundle() },
- foreignKeys.map { it.toBundle() })
-
- fun isUnique(columns: List<String>): Boolean {
- return if (primaryKey.columnNames.size == columns.size
- && primaryKey.columnNames.containsAll(columns)) {
- true
- } else {
- indices.any { index ->
- index.unique
- && index.fields.size == columns.size
- && index.columnNames.containsAll(columns)
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
deleted file mode 100644
index 43e0605..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Field.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.ext.isNonNull
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.migration.bundle.FieldBundle
-import android.arch.persistence.room.parser.Collate
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.solver.types.CursorValueReader
-import android.arch.persistence.room.solver.types.StatementValueBinder
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.Element
-import javax.lang.model.type.TypeMirror
-// used in cache matching, must stay as a data class or implement equals
-data class Field(val element: Element, val name: String, val type: TypeMirror,
- var affinity: SQLTypeAffinity?,
- val collate: Collate? = null,
- val columnName: String = name,
- /* means that this field does not belong to parent, instead, it belongs to a
- * embedded child of the main Pojo*/
- val parent: EmbeddedField? = null,
- // index might be removed when being merged into an Entity
- var indexed: Boolean = false) : HasSchemaIdentity {
- lateinit var getter: FieldGetter
- lateinit var setter: FieldSetter
- // binds the field into a statement
- var statementBinder: StatementValueBinder? = null
- // reads this field from a cursor column
- var cursorValueReader: CursorValueReader? = null
- val typeName: TypeName by lazy { type.typeName() }
-
- /** Whether the table column for this field should be NOT NULL */
- val nonNull = element.isNonNull() && (parent == null || parent.isNonNullRecursively())
-
- override fun getIdKey(): String {
- // we don't get the collate information from sqlite so ignoring it here.
- return "$columnName-${affinity?.name ?: SQLTypeAffinity.TEXT.name}-$nonNull"
- }
-
- /**
- * Used when reporting errors on duplicate names
- */
- fun getPath(): String {
- return if (parent == null) {
- name
- } else {
- "${parent.field.getPath()} > $name"
- }
- }
-
- private val pathWithDotNotation: String by lazy {
- if (parent == null) {
- name
- } else {
- "${parent.field.pathWithDotNotation}.$name"
- }
- }
-
- /**
- * List of names that include variations.
- * e.g. if it is mUser, user is added to the list
- * or if it is isAdmin, admin is added to the list
- */
- val nameWithVariations by lazy {
- val result = arrayListOf(name)
- if (name.length > 1) {
- if (name.startsWith('_')) {
- result.add(name.substring(1))
- }
- if (name.startsWith("m") && name[1].isUpperCase()) {
- result.add(name.substring(1).decapitalize())
- }
-
- if (typeName == TypeName.BOOLEAN || typeName == TypeName.BOOLEAN.box()) {
- if (name.length > 2 && name.startsWith("is") && name[2].isUpperCase()) {
- result.add(name.substring(2).decapitalize())
- }
- if (name.length > 3 && name.startsWith("has") && name[3].isUpperCase()) {
- result.add(name.substring(3).decapitalize())
- }
- }
- }
- result
- }
-
- val getterNameWithVariations by lazy {
- nameWithVariations.map { "get${it.capitalize()}" } +
- if (typeName == TypeName.BOOLEAN || typeName == TypeName.BOOLEAN.box()) {
- nameWithVariations.flatMap {
- listOf("is${it.capitalize()}", "has${it.capitalize()}")
- }
- } else {
- emptyList()
- }
- }
-
- val setterNameWithVariations by lazy {
- nameWithVariations.map { "set${it.capitalize()}" }
- }
-
- /**
- * definition to be used in create query
- */
- fun databaseDefinition(autoIncrementPKey: Boolean): String {
- val columnSpec = StringBuilder("")
- if (autoIncrementPKey) {
- columnSpec.append(" PRIMARY KEY AUTOINCREMENT")
- }
- if (nonNull) {
- columnSpec.append(" NOT NULL")
- }
- if (collate != null) {
- columnSpec.append(" COLLATE ${collate.name}")
- }
- return "`$columnName` ${(affinity ?: SQLTypeAffinity.TEXT).name}$columnSpec"
- }
-
- fun toBundle(): FieldBundle = FieldBundle(pathWithDotNotation, columnName,
- affinity?.name ?: SQLTypeAffinity.TEXT.name, nonNull
- )
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldGetter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldGetter.kt
deleted file mode 100644
index 70a9bc3..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldGetter.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.TypeName
-import javax.lang.model.type.TypeMirror
-
-data class FieldGetter(val name: String, val type: TypeMirror, val callType: CallType) {
- fun writeGet(ownerVar: String, outVar: String, builder: CodeBlock.Builder) {
- val stmt = when (callType) {
- CallType.FIELD -> "final $T $L = $L.$L"
- CallType.METHOD -> "final $T $L = $L.$L()"
- CallType.CONSTRUCTOR -> null
- }
- stmt?.let {
- builder.addStatement(stmt, TypeName.get(type), outVar, ownerVar, name)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldSetter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldSetter.kt
deleted file mode 100644
index 5aa1806..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldSetter.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.ext.L
-import com.squareup.javapoet.CodeBlock
-import javax.lang.model.type.TypeMirror
-
-data class FieldSetter(val name: String, val type: TypeMirror, val callType: CallType) {
- fun writeSet(ownerVar: String, inVar: String, builder: CodeBlock.Builder) {
- val stmt = when (callType) {
- CallType.FIELD -> "$L.$L = $L"
- CallType.METHOD -> "$L.$L($L)"
- CallType.CONSTRUCTOR -> null
- }
- stmt?.let {
- builder.addStatement(stmt, ownerVar, name, inVar)
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldWithIndex.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldWithIndex.kt
deleted file mode 100644
index b30dcc9..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/FieldWithIndex.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.vo
-
-/**
- * A common value object when we need to associate a Field with an Index
- * variable.
- * <p>
- * If we are sure that the field will be there at compile time, we set it to always Exists so that
- * the generated code does not check for -1 column indices.
- */
-data class FieldWithIndex(val field: Field, val indexVar: String, val alwaysExists: Boolean) {
- companion object {
- fun byOrder(fields: List<Field>): List<FieldWithIndex> {
- return fields.mapIndexed { index, field ->
- FieldWithIndex(field = field,
- indexVar = "${index + 1}",
- alwaysExists = true)
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
deleted file mode 100644
index 833de97..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKey.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.migration.bundle.ForeignKeyBundle
-
-/**
- * Keeps information about a foreign key.
- */
-data class ForeignKey(val parentTable: String,
- val parentColumns: List<String>,
- val childFields: List<Field>,
- val onDelete: ForeignKeyAction,
- val onUpdate: ForeignKeyAction,
- val deferred: Boolean) : HasSchemaIdentity {
- override fun getIdKey(): String {
- return parentTable +
- "-${parentColumns.joinToString(",")}" +
- "-${childFields.joinToString(",") {it.columnName}}" +
- "-${onDelete.sqlName}" +
- "-${onUpdate.sqlName}" +
- "-$deferred"
- }
-
- fun databaseDefinition(): String {
- return "FOREIGN KEY(${joinEscaped(childFields.map { it.columnName })})" +
- " REFERENCES `$parentTable`(${joinEscaped(parentColumns)})" +
- " ON UPDATE ${onUpdate.sqlName}" +
- " ON DELETE ${onDelete.sqlName}" +
- " ${deferredDeclaration()}"
- }
-
- private fun deferredDeclaration(): String {
- return if (deferred) {
- "DEFERRABLE INITIALLY DEFERRED"
- } else {
- ""
- }
- }
-
- private fun joinEscaped(values: Iterable<String>) = values.joinToString(", ") { "`$it`" }
-
- fun toBundle(): ForeignKeyBundle = ForeignKeyBundle(
- parentTable, onDelete.sqlName, onUpdate.sqlName,
- childFields.map { it.columnName },
- parentColumns
- )
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKeyAction.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKeyAction.kt
deleted file mode 100644
index c6ed9da..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ForeignKeyAction.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.ForeignKey
-
-/**
- * Compiler representation of ForeignKey#Action.
- */
-enum class ForeignKeyAction(val annotationValue: Int, val sqlName: String) {
- NO_ACTION(ForeignKey.NO_ACTION, "NO ACTION"),
- RESTRICT(ForeignKey.RESTRICT, "RESTRICT"),
- SET_NULL(ForeignKey.SET_NULL, "SET NULL"),
- SET_DEFAULT(ForeignKey.SET_DEFAULT, "SET DEFAULT"),
- CASCADE(ForeignKey.CASCADE, "CASCADE");
- companion object {
- private val mapping by lazy {
- ForeignKeyAction.values().associateBy { it.annotationValue }
- }
- fun fromAnnotationValue(value: Int?) = mapping[value]
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
deleted file mode 100644
index b4952cf..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Index.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.migration.bundle.BundleUtil
-import android.arch.persistence.room.migration.bundle.IndexBundle
-
-/**
- * Represents a processed index.
- */
-data class Index(val name: String, val unique: Boolean, val fields: List<Field>) :
- HasSchemaIdentity {
- companion object {
- // should match the value in TableInfo.Index.DEFAULT_PREFIX
- const val DEFAULT_PREFIX = "index_"
- }
-
- override fun getIdKey(): String {
- return "$unique-$name-${fields.joinToString(",") { it.columnName }}"
- }
-
- fun createQuery(tableName: String): String {
- val uniqueSQL = if (unique) {
- "UNIQUE"
- } else {
- ""
- }
- return """
- CREATE $uniqueSQL INDEX `$name`
- ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`" }})
- """.trimIndent().replace("\n", " ")
- }
-
- val columnNames by lazy { fields.map { it.columnName } }
-
- fun toBundle(): IndexBundle = IndexBundle(name, unique, fields.map { it.columnName },
- createQuery(BundleUtil.TABLE_NAME_PLACEHOLDER))
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/InsertionMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/InsertionMethod.kt
deleted file mode 100644
index 19573cf..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/InsertionMethod.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.OnConflictStrategy
-import android.arch.persistence.room.ext.typeName
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.TypeMirror
-
-data class InsertionMethod(val element: ExecutableElement, val name: String,
- @OnConflictStrategy val onConflict: Int,
- val entities: Map<String, Entity>, val returnType: TypeMirror,
- val insertionType: Type?,
- val parameters: List<ShortcutQueryParameter>) {
- fun insertMethodTypeFor(param: ShortcutQueryParameter): Type {
- return if (insertionType == Type.INSERT_VOID || insertionType == null) {
- Type.INSERT_VOID
- } else if (!param.isMultiple) {
- Type.INSERT_SINGLE_ID
- } else {
- insertionType
- }
- }
-
- enum class Type(
- // methodName matches EntityInsertionAdapter methods
- val methodName: String, val returnTypeName: TypeName) {
- INSERT_VOID("insert", TypeName.VOID), // return void
- INSERT_SINGLE_ID("insertAndReturnId", TypeName.LONG), // return long
- INSERT_ID_ARRAY("insertAndReturnIdsArray",
- ArrayTypeName.of(TypeName.LONG)), // return long[]
- INSERT_ID_ARRAY_BOX("insertAndReturnIdsArrayBox",
- ArrayTypeName.of(TypeName.LONG.box())), // return Long[]
- INSERT_ID_LIST("insertAndReturnIdsList", // return List<Long>
- ParameterizedTypeName.get(List::class.typeName(), TypeName.LONG.box()))
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Pojo.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Pojo.kt
deleted file mode 100644
index 34ec61c..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Pojo.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.processor.EntityProcessor
-import com.google.auto.common.MoreElements
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-
-/**
- * A class is turned into a Pojo if it is used in a query response.
- */
-open class Pojo(
- val element: TypeElement,
- val type: DeclaredType,
- val fields: List<Field>,
- val embeddedFields: List<EmbeddedField>,
- val relations: List<Relation>,
- val constructor: Constructor? = null) {
- val typeName: TypeName by lazy { type.typeName() }
-
- /**
- * All table names that are somehow accessed by this Pojo.
- * Might be via Embedded or Relation.
- */
- fun accessedTableNames(): List<String> {
- val entityAnnotation = MoreElements.getAnnotationMirror(element,
- android.arch.persistence.room.Entity::class.java).orNull()
- return if (entityAnnotation != null) {
- listOf(EntityProcessor.extractTableName(element, entityAnnotation))
- } else {
- embeddedFields.flatMap {
- it.pojo.accessedTableNames()
- } + relations.map {
- it.entity.tableName
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PojoMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PojoMethod.kt
deleted file mode 100644
index 2a59ef8..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PojoMethod.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2018 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.vo
-
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.ExecutableType
-
-/**
- * An executable element processed as member of a class (pojo or entity)
- */
-class PojoMethod(
- val element: ExecutableElement,
- val resolvedType: ExecutableType,
- val name: String)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
deleted file mode 100644
index 1d76d40..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/PrimaryKey.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.migration.bundle.PrimaryKeyBundle
-import javax.lang.model.element.Element
-
-/**
- * Represents a PrimaryKey for an Entity.
- */
-data class PrimaryKey(val declaredIn: Element?, val fields: List<Field>,
- val autoGenerateId: Boolean) : HasSchemaIdentity {
- companion object {
- val MISSING = PrimaryKey(null, emptyList(), false)
- }
-
- val columnNames by lazy { fields.map { it.columnName } }
-
- fun toHumanReadableString(): String {
- return "PrimaryKey[" +
- fields.joinToString(separator = ", ", transform = Field::getPath) + "]"
- }
-
- fun toBundle(): PrimaryKeyBundle = PrimaryKeyBundle(
- autoGenerateId, fields.map { it.columnName })
-
- override fun getIdKey(): String {
- return "$autoGenerateId-${fields.map { it.columnName }}"
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
deleted file mode 100644
index 8898d80..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.TypeMirror
-
-/**
- * A class that holds information about a QueryMethod.
- * It is self sufficient and must have all generics etc resolved once created.
- */
-data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
- val returnType: TypeMirror, val parameters: List<QueryParameter>,
- val inTransaction: Boolean,
- val queryResultBinder: QueryResultBinder) {
- val sectionToParamMapping by lazy {
- query.bindSections.map {
- if (it.text.trim() == "?") {
- Pair(it, parameters.firstOrNull())
- } else if (it.text.startsWith(":")) {
- val subName = it.text.substring(1)
- Pair(it, parameters.firstOrNull {
- it.sqlName == subName
- })
- } else {
- Pair(it, null)
- }
- }
- }
-
- val returnsValue by lazy {
- returnType.typeName() != TypeName.VOID
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryParameter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryParameter.kt
deleted file mode 100644
index c33deac..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryParameter.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import android.arch.persistence.room.solver.query.parameter.QueryParameterAdapter
-import javax.lang.model.type.TypeMirror
-
-/**
- * Holds the parameter for a {@link QueryMethod}.
- */
-data class QueryParameter(
- // this is name seen by java
- val name: String,
- // this is the name used in the query. Might be different for kotlin queries
- val sqlName: String,
- val type: TypeMirror,
- val queryParamAdapter: QueryParameterAdapter?)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RawQueryMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RawQueryMethod.kt
deleted file mode 100644
index 5099906..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RawQueryMethod.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2018 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.vo
-
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.query.result.QueryResultBinder
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.TypeMirror
-
-/**
- * A class that holds information about a method annotated with RawQuery.
- * It is self sufficient and must have all generics etc resolved once created.
- */
-data class RawQueryMethod(
- val element: ExecutableElement,
- val name: String,
- val returnType: TypeMirror,
- val inTransaction: Boolean,
- val observedTableNames: Set<String>,
- val runtimeQueryParam: RuntimeQueryParameter?,
- val queryResultBinder: QueryResultBinder) {
- val returnsValue by lazy {
- returnType.typeName() != TypeName.VOID
- }
-
- data class RuntimeQueryParameter(
- val paramName: String,
- val type: TypeName) {
- fun isString() = CommonTypeNames.STRING == type
- fun isSupportQuery() = SupportDbTypeNames.QUERY == type
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt
deleted file mode 100644
index 512770a..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Relation.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.ext.typeName
-import javax.lang.model.type.TypeMirror
-
-/**
- * Value object created from processing a @Relation annotation.
- */
-class Relation(
- val entity: Entity,
- // return type. e..g. String in @Relation List<String>
- val pojoType: TypeMirror,
- // field in Pojo that holds these relations (e.g. List<Pet> pets)
- val field: Field,
- // the parent field referenced for matching
- val parentField: Field,
- // the field referenced for querying. does not need to be in the response but the query
- // we generate always has it in the response.
- val entityField: Field,
- // the projection for the query
- val projection: List<String>) {
-
- val pojoTypeName by lazy { pojoType.typeName() }
-
- fun createLoadAllSql(): String {
- val resultFields = projection.toSet() + entityField.columnName
- return createSelect(resultFields)
- }
-
- private fun createSelect(resultFields: Set<String>): String {
- return "SELECT ${resultFields.joinToString(",") {"`$it`"}}" +
- " FROM `${entity.tableName}`" +
- " WHERE `${entityField.columnName}` IN (:args)"
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RelationCollector.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RelationCollector.kt
deleted file mode 100644
index add8fb1..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/RelationCollector.kt
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.parser.SqlParser
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER
-import android.arch.persistence.room.processor.ProcessorErrors.relationAffinityMismatch
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.solver.query.result.RowAdapter
-import android.arch.persistence.room.solver.query.result.SingleColumnRowAdapter
-import android.arch.persistence.room.verifier.DatabaseVerificaitonErrors
-import android.arch.persistence.room.writer.QueryWriter
-import android.arch.persistence.room.writer.RelationCollectorMethodWriter
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import stripNonJava
-import java.util.ArrayList
-import java.util.HashSet
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-
-/**
- * Internal class that is used to manage fetching 1/N to N relationships.
- */
-data class RelationCollector(val relation: Relation,
- val affinity: SQLTypeAffinity,
- val mapTypeName: ParameterizedTypeName,
- val keyTypeName: TypeName,
- val collectionTypeName: ParameterizedTypeName,
- val queryWriter: QueryWriter,
- val rowAdapter: RowAdapter,
- val loadAllQuery: ParsedQuery) {
- // set when writing the code generator in writeInitCode
- lateinit var varName: String
-
- fun writeInitCode(scope: CodeGenScope) {
- val tmpVar = scope.getTmpVar(
- "_collection${relation.field.getPath().stripNonJava().capitalize()}")
- scope.builder().addStatement("final $T $L = new $T()", mapTypeName, tmpVar, mapTypeName)
- varName = tmpVar
- }
-
- // called after reading each item to extract the key if it exists
- fun writeReadParentKeyCode(cursorVarName: String, itemVar: String,
- fieldsWithIndices: List<FieldWithIndex>, scope: CodeGenScope) {
- val indexVar = fieldsWithIndices.firstOrNull {
- it.field === relation.parentField
- }?.indexVar
- scope.builder().apply {
- readKey(
- cursorVarName = cursorVarName,
- indexVar = indexVar,
- scope = scope
- ) { tmpVar ->
- val tmpCollectionVar = scope.getTmpVar("_tmpCollection")
- addStatement("$T $L = $L.get($L)", collectionTypeName, tmpCollectionVar,
- varName, tmpVar)
- beginControlFlow("if($L == null)", tmpCollectionVar).apply {
- addStatement("$L = new $T()", tmpCollectionVar, collectionTypeName)
- addStatement("$L.put($L, $L)", varName, tmpVar, tmpCollectionVar)
- }
- endControlFlow()
- // set it on the item
- relation.field.setter.writeSet(itemVar, tmpCollectionVar, this)
- }
- }
- }
-
- fun writeCollectionCode(scope: CodeGenScope) {
- val method = scope.writer
- .getOrCreateMethod(RelationCollectorMethodWriter(this))
- scope.builder().apply {
- addStatement("$N($L)", method, varName)
- }
- }
-
- fun readKey(cursorVarName: String, indexVar: String?, scope: CodeGenScope,
- postRead: CodeBlock.Builder.(String) -> Unit) {
- val cursorGetter = when (affinity) {
- SQLTypeAffinity.INTEGER -> "getLong"
- SQLTypeAffinity.REAL -> "getDouble"
- SQLTypeAffinity.TEXT -> "getString"
- SQLTypeAffinity.BLOB -> "getBlob"
- else -> {
- "getString"
- }
- }
- scope.builder().apply {
- beginControlFlow("if (!$L.isNull($L))", cursorVarName, indexVar).apply {
- val tmpVar = scope.getTmpVar("_tmpKey")
- addStatement("final $T $L = $L.$L($L)", keyTypeName,
- tmpVar, cursorVarName, cursorGetter, indexVar)
- this.postRead(tmpVar)
- }
- endControlFlow()
- }
- }
-
- companion object {
- fun createCollectors(
- baseContext: Context,
- relations: List<Relation>
- ): List<RelationCollector> {
- return relations.map { relation ->
- // decide on the affinity
- val context = baseContext.fork(relation.field.element)
- val parentAffinity = relation.parentField.cursorValueReader?.affinity()
- val childAffinity = relation.entityField.cursorValueReader?.affinity()
- val affinity = if (parentAffinity != null && parentAffinity == childAffinity) {
- parentAffinity
- } else {
- context.logger.w(Warning.RELATION_TYPE_MISMATCH, relation.field.element,
- relationAffinityMismatch(
- parentColumn = relation.parentField.columnName,
- childColumn = relation.entityField.columnName,
- parentAffinity = parentAffinity,
- childAffinity = childAffinity))
- SQLTypeAffinity.TEXT
- }
- val keyType = keyTypeFor(context, affinity)
- val collectionTypeName = if (relation.field.typeName is ParameterizedTypeName) {
- val paramType = relation.field.typeName as ParameterizedTypeName
- if (paramType.rawType == CommonTypeNames.LIST) {
- ParameterizedTypeName.get(ClassName.get(ArrayList::class.java),
- relation.pojoTypeName)
- } else if (paramType.rawType == CommonTypeNames.SET) {
- ParameterizedTypeName.get(ClassName.get(HashSet::class.java),
- relation.pojoTypeName)
- } else {
- ParameterizedTypeName.get(ClassName.get(ArrayList::class.java),
- relation.pojoTypeName)
- }
- } else {
- ParameterizedTypeName.get(ClassName.get(ArrayList::class.java),
- relation.pojoTypeName)
- }
-
- val canUseArrayMap = context.processingEnv.elementUtils
- .getTypeElement(AndroidTypeNames.ARRAY_MAP.toString()) != null
- val mapClass = if (canUseArrayMap) {
- AndroidTypeNames.ARRAY_MAP
- } else {
- ClassName.get(java.util.HashMap::class.java)
- }
- val tmpMapType = ParameterizedTypeName.get(mapClass, keyType, collectionTypeName)
- val keyTypeMirror = keyTypeMirrorFor(context, affinity)
- val set = context.processingEnv.elementUtils.getTypeElement("java.util.Set")
- val keySet = context.processingEnv.typeUtils.getDeclaredType(set, keyTypeMirror)
- val loadAllQuery = relation.createLoadAllSql()
- val parsedQuery = SqlParser.parse(loadAllQuery)
- context.checker.check(parsedQuery.errors.isEmpty(), relation.field.element,
- parsedQuery.errors.joinToString("\n"))
- if (parsedQuery.errors.isEmpty()) {
- val resultInfo = context.databaseVerifier?.analyze(loadAllQuery)
- parsedQuery.resultInfo = resultInfo
- if (resultInfo?.error != null) {
- context.logger.e(relation.field.element,
- DatabaseVerificaitonErrors.cannotVerifyQuery(resultInfo.error))
- }
- }
- val resultInfo = parsedQuery.resultInfo
-
- val queryParam = QueryParameter(
- name = RelationCollectorMethodWriter.KEY_SET_VARIABLE,
- sqlName = RelationCollectorMethodWriter.KEY_SET_VARIABLE,
- type = keySet,
- queryParamAdapter =
- context.typeAdapterStore.findQueryParameterAdapter(keySet))
- val queryWriter = QueryWriter(
- parameters = listOf(queryParam),
- sectionToParamMapping = listOf(Pair(parsedQuery.bindSections.first(),
- queryParam)),
- query = parsedQuery
- )
-
- // row adapter that matches full response
- fun getDefaultRowAdapter(): RowAdapter? {
- return context.typeAdapterStore.findRowAdapter(relation.pojoType, parsedQuery)
- }
- val rowAdapter = if (relation.projection.size == 1 && resultInfo != null &&
- (resultInfo.columns.size == 1 || resultInfo.columns.size == 2)) {
- // check for a column adapter first
- val cursorReader = context.typeAdapterStore.findCursorValueReader(
- relation.pojoType, resultInfo.columns.first().type)
- if (cursorReader == null) {
- getDefaultRowAdapter()
- } else {
- context.logger.d("Choosing cursor adapter for the return value since" +
- " the query returns only 1 or 2 columns and there is a cursor" +
- " adapter for the return type.")
- SingleColumnRowAdapter(cursorReader)
- }
- } else {
- getDefaultRowAdapter()
- }
-
- if (rowAdapter == null) {
- context.logger.e(relation.field.element, CANNOT_FIND_QUERY_RESULT_ADAPTER)
- null
- } else {
- RelationCollector(
- relation = relation,
- affinity = affinity,
- mapTypeName = tmpMapType,
- keyTypeName = keyType,
- collectionTypeName = collectionTypeName,
- queryWriter = queryWriter,
- rowAdapter = rowAdapter,
- loadAllQuery = parsedQuery
- )
- }
- }.filterNotNull()
- }
-
- private fun keyTypeMirrorFor(context: Context, affinity: SQLTypeAffinity): TypeMirror {
- val types = context.processingEnv.typeUtils
- val elements = context.processingEnv.elementUtils
- return when (affinity) {
- SQLTypeAffinity.INTEGER -> elements.getTypeElement("java.lang.Long").asType()
- SQLTypeAffinity.REAL -> elements.getTypeElement("java.lang.Double").asType()
- SQLTypeAffinity.TEXT -> context.COMMON_TYPES.STRING
- SQLTypeAffinity.BLOB -> types.getArrayType(types.getPrimitiveType(TypeKind.BYTE))
- else -> {
- context.COMMON_TYPES.STRING
- }
- }
- }
-
- private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): TypeName {
- return when (affinity) {
- SQLTypeAffinity.INTEGER -> TypeName.LONG.box()
- SQLTypeAffinity.REAL -> TypeName.DOUBLE.box()
- SQLTypeAffinity.TEXT -> TypeName.get(String::class.java)
- SQLTypeAffinity.BLOB -> ArrayTypeName.of(TypeName.BYTE)
- else -> {
- // no affinity select from type
- context.COMMON_TYPES.STRING.typeName()
- }
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt
deleted file mode 100644
index 06c9ff5..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/SchemaIdentityKey.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 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.vo
-
-import org.apache.commons.codec.digest.DigestUtils
-import java.util.Locale
-
-interface HasSchemaIdentity {
- fun getIdKey(): String
-}
-
-/**
- * A class that can be converted into a unique identifier for an object
- */
-class SchemaIdentityKey {
- companion object {
- private val SEPARATOR = "?:?"
- private val ENGLISH_SORT = Comparator<String> { o1, o2 ->
- o1.toLowerCase(Locale.ENGLISH).compareTo(o2.toLowerCase(Locale.ENGLISH))
- }
- }
-
- private val sb = StringBuilder()
- fun append(identity: HasSchemaIdentity) {
- append(identity.getIdKey())
- }
-
- fun appendSorted(identities: List<HasSchemaIdentity>) {
- identities.map { it.getIdKey() }.sortedWith(ENGLISH_SORT).forEach {
- append(it)
- }
- }
-
- fun hash() = DigestUtils.md5Hex(sb.toString())
- fun append(identity: String) {
- sb.append(identity).append(SEPARATOR)
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ShortcutMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ShortcutMethod.kt
deleted file mode 100644
index 121f993..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ShortcutMethod.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.vo
-
-import javax.lang.model.element.ExecutableElement
-
-/**
- * Base class for shortcut methods in @DAO.
- */
-abstract class ShortcutMethod(val element: ExecutableElement, val name: String,
- val entities: Map<String, Entity>, val returnCount: Boolean,
- val parameters: List<ShortcutQueryParameter>)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ShortcutQueryParameter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ShortcutQueryParameter.kt
deleted file mode 100644
index db8a0f5..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/ShortcutQueryParameter.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 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.vo
-
-import javax.lang.model.type.TypeMirror
-
-/**
- * Parameters used in DAO methods that are annotated with Insert, Delete, Update.
- */
-data class ShortcutQueryParameter(val name: String, val type: TypeMirror,
- val entityType: TypeMirror?, val isMultiple: Boolean) {
- /**
- * Method name in entity insertion or update adapter.
- */
- fun handleMethodName(): String {
- return if (isMultiple) {
- "handleMultiple"
- } else {
- "handle"
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt
deleted file mode 100644
index 10e1889..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.vo
-
-import javax.lang.model.element.ExecutableElement
-
-class TransactionMethod(val element: ExecutableElement, val name: String, val callType: CallType) {
- enum class CallType {
- CONCRETE, DEFAULT_JAVA8, DEFAULT_KOTLIN
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/UpdateMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/UpdateMethod.kt
deleted file mode 100644
index ed8eb8b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/UpdateMethod.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.OnConflictStrategy
-import javax.lang.model.element.ExecutableElement
-
-class UpdateMethod(element: ExecutableElement, name: String,
- entities: Map<String, Entity>, returnCount: Boolean,
- parameters: List<ShortcutQueryParameter>,
- @OnConflictStrategy val onConflictStrategy: Int) : ShortcutMethod(
- element, name, entities, returnCount, parameters)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
deleted file mode 100644
index dd154c4..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.vo
-
-/**
- * Internal representation of supported warnings
- */
-enum class Warning(val publicKey: String) {
- ALL("ALL"),
- CURSOR_MISMATCH("ROOM_CURSOR_MISMATCH"),
- MISSING_JAVA_TMP_DIR("ROOM_MISSING_JAVA_TMP_DIR"),
- CANNOT_CREATE_VERIFICATION_DATABASE("ROOM_CANNOT_CREATE_VERIFICATION_DATABASE"),
- PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED("ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED"),
- INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED("ROOM_EMBEDDED_INDEX_IS_DROPPED"),
- INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED("ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED"),
- INDEX_FROM_PARENT_IS_DROPPED("ROOM_PARENT_INDEX_IS_DROPPED"),
- INDEX_FROM_PARENT_FIELD_IS_DROPPED("ROOM_PARENT_FIELD_INDEX_IS_DROPPED"),
- RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH"),
- MISSING_SCHEMA_LOCATION("ROOM_MISSING_SCHEMA_LOCATION"),
- MISSING_INDEX_ON_FOREIGN_KEY_CHILD("ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"),
- RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
- DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR");
-
- companion object {
- val PUBLIC_KEY_MAP = Warning.values().associateBy { it.publicKey }
- fun fromPublicKey(publicKey: String): Warning? {
- return PUBLIC_KEY_MAP[publicKey.toUpperCase()]
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt
deleted file mode 100644
index d4dd116..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/ClassWriter.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.RoomProcessor
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope.Companion.CLASS_PROPERTY_PREFIX
-import com.squareup.javapoet.AnnotationSpec
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.annotation.processing.ProcessingEnvironment
-
-/**
- * Base class for all writers that can produce a class.
- */
-abstract class ClassWriter(private val className: ClassName) {
- private val sharedFieldSpecs = mutableMapOf<String, FieldSpec>()
- private val sharedMethodSpecs = mutableMapOf<String, MethodSpec>()
- private val sharedFieldNames = mutableSetOf<String>()
- private val sharedMethodNames = mutableSetOf<String>()
-
- abstract fun createTypeSpecBuilder(): TypeSpec.Builder
-
- fun write(processingEnv: ProcessingEnvironment) {
- val builder = createTypeSpecBuilder()
- sharedFieldSpecs.values.forEach { builder.addField(it) }
- sharedMethodSpecs.values.forEach { builder.addMethod(it) }
- addGeneratedAnnotationIfAvailable(builder, processingEnv)
- addSuppressUnchecked(builder)
- JavaFile.builder(className.packageName(), builder.build())
- .build()
- .writeTo(processingEnv.filer)
- }
-
- private fun addSuppressUnchecked(builder: TypeSpec.Builder) {
- val suppressSpec = AnnotationSpec.builder(SuppressWarnings::class.typeName()).addMember(
- "value",
- S,
- "unchecked"
- ).build()
- builder.addAnnotation(suppressSpec)
- }
-
- private fun addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder: TypeSpec.Builder,
- processingEnv: ProcessingEnvironment) {
- val generatedAnnotationAvailable = processingEnv
- .elementUtils
- .getTypeElement(GENERATED_PACKAGE + "." + GENERATED_NAME) != null
- if (generatedAnnotationAvailable) {
- val className = ClassName.get(GENERATED_PACKAGE, GENERATED_NAME)
- val generatedAnnotationSpec =
- AnnotationSpec.builder(className).addMember(
- "value",
- S,
- RoomProcessor::class.java.canonicalName).build()
- adapterTypeSpecBuilder.addAnnotation(generatedAnnotationSpec)
- }
- }
-
- private fun makeUnique(set: MutableSet<String>, value: String): String {
- if (!value.startsWith(CLASS_PROPERTY_PREFIX)) {
- return makeUnique(set, "$CLASS_PROPERTY_PREFIX$value")
- }
- if (set.add(value)) {
- return value
- }
- var index = 1
- while (true) {
- if (set.add("${value}_$index")) {
- return "${value}_$index"
- }
- index++
- }
- }
-
- fun getOrCreateField(sharedField: SharedFieldSpec): FieldSpec {
- return sharedFieldSpecs.getOrPut(sharedField.getUniqueKey(), {
- sharedField.build(this, makeUnique(sharedFieldNames, sharedField.baseName))
- })
- }
-
- fun getOrCreateMethod(sharedMethod: SharedMethodSpec): MethodSpec {
- return sharedMethodSpecs.getOrPut(sharedMethod.getUniqueKey(), {
- sharedMethod.build(this, makeUnique(sharedMethodNames, sharedMethod.baseName))
- })
- }
-
- abstract class SharedFieldSpec(val baseName: String, val type: TypeName) {
-
- abstract fun getUniqueKey(): String
-
- abstract fun prepare(writer: ClassWriter, builder: FieldSpec.Builder)
-
- fun build(classWriter: ClassWriter, name: String): FieldSpec {
- val builder = FieldSpec.builder(type, name)
- prepare(classWriter, builder)
- return builder.build()
- }
- }
-
- abstract class SharedMethodSpec(val baseName: String) {
-
- abstract fun getUniqueKey(): String
- abstract fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder)
-
- fun build(writer: ClassWriter, name: String): MethodSpec {
- val builder = MethodSpec.methodBuilder(name)
- prepare(name, writer, builder)
- return builder.build()
- }
- }
-
- companion object {
- private const val GENERATED_PACKAGE = "javax.annotation"
- private const val GENERATED_NAME = "Generated"
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
deleted file mode 100644
index f0d2771..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
+++ /dev/null
@@ -1,605 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.QueryType
-import android.arch.persistence.room.processor.OnConflictProcessor
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Dao
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.InsertionMethod
-import android.arch.persistence.room.vo.QueryMethod
-import android.arch.persistence.room.vo.RawQueryMethod
-import android.arch.persistence.room.vo.ShortcutMethod
-import android.arch.persistence.room.vo.TransactionMethod
-import com.google.auto.common.MoreTypes
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import org.jetbrains.kotlin.load.java.JvmAbi
-import stripNonJava
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.ElementKind
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier.FINAL
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PUBLIC
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeKind
-
-/**
- * Creates the implementation for a class annotated with Dao.
- */
-class DaoWriter(val dao: Dao, val processingEnv: ProcessingEnvironment)
- : ClassWriter(dao.typeName) {
- private val declaredDao = MoreTypes.asDeclared(dao.element.asType())
-
- companion object {
- // TODO nothing prevents this from conflicting, we should fix.
- val dbField: FieldSpec = FieldSpec
- .builder(RoomTypeNames.ROOM_DB, "__db", PRIVATE, FINAL)
- .build()
-
- private fun typeNameToFieldName(typeName: TypeName?): String {
- if (typeName is ClassName) {
- return typeName.simpleName()
- } else {
- return typeName.toString().replace('.', '_').stripNonJava()
- }
- }
- }
-
- override fun createTypeSpecBuilder(): TypeSpec.Builder {
- val builder = TypeSpec.classBuilder(dao.implTypeName)
- /**
- * if delete / update query method wants to return modified rows, we need prepared query.
- * in that case, if args are dynamic, we cannot re-use the query, if not, we should re-use
- * it. this requires more work but creates good performance.
- */
- val groupedDeleteUpdate = dao.queryMethods
- .filter { it.query.type == QueryType.DELETE || it.query.type == QueryType.UPDATE }
- .groupBy { it.parameters.any { it.queryParamAdapter?.isMultiple ?: true } }
- // delete queries that can be prepared ahead of time
- val preparedDeleteOrUpdateQueries = groupedDeleteUpdate[false] ?: emptyList()
- // delete queries that must be rebuild every single time
- val oneOffDeleteOrUpdateQueries = groupedDeleteUpdate[true] ?: emptyList()
- val shortcutMethods = createInsertionMethods() +
- createDeletionMethods() + createUpdateMethods() + createTransactionMethods() +
- createPreparedDeleteOrUpdateQueries(preparedDeleteOrUpdateQueries)
-
- builder.apply {
- addModifiers(PUBLIC)
- if (dao.element.kind == ElementKind.INTERFACE) {
- addSuperinterface(dao.typeName)
- } else {
- superclass(dao.typeName)
- }
- addField(dbField)
- val dbParam = ParameterSpec
- .builder(dao.constructorParamType ?: dbField.type, dbField.name).build()
-
- addMethod(createConstructor(dbParam, shortcutMethods, dao.constructorParamType != null))
-
- shortcutMethods.forEach {
- addMethod(it.methodImpl)
- }
-
- dao.queryMethods.filter { it.query.type == QueryType.SELECT }.forEach { method ->
- addMethod(createSelectMethod(method))
- }
- oneOffDeleteOrUpdateQueries.forEach {
- addMethod(createDeleteOrUpdateQueryMethod(it))
- }
- dao.rawQueryMethods.forEach {
- addMethod(createRawQueryMethod(it))
- }
- }
- return builder
- }
-
- private fun createPreparedDeleteOrUpdateQueries(
- preparedDeleteQueries: List<QueryMethod>): List<PreparedStmtQuery> {
- return preparedDeleteQueries.map { method ->
- val fieldSpec = getOrCreateField(PreparedStatementField(method))
- val queryWriter = QueryWriter(method)
- val fieldImpl = PreparedStatementWriter(queryWriter)
- .createAnonymous(this@DaoWriter, dbField)
- val methodBody = createPreparedDeleteQueryMethodBody(method, fieldSpec, queryWriter)
- PreparedStmtQuery(mapOf(PreparedStmtQuery.NO_PARAM_FIELD
- to (fieldSpec to fieldImpl)), methodBody)
- }
- }
-
- private fun createPreparedDeleteQueryMethodBody(
- method: QueryMethod,
- preparedStmtField: FieldSpec,
- queryWriter: QueryWriter
- ): MethodSpec {
- val scope = CodeGenScope(this)
- val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
- val stmtName = scope.getTmpVar("_stmt")
- addStatement("final $T $L = $N.acquire()",
- SupportDbTypeNames.SQLITE_STMT, stmtName, preparedStmtField)
- addStatement("$N.beginTransaction()", dbField)
- beginControlFlow("try").apply {
- val bindScope = scope.fork()
- queryWriter.bindArgs(stmtName, emptyList(), bindScope)
- addCode(bindScope.builder().build())
- if (method.returnsValue) {
- val resultVar = scope.getTmpVar("_result")
- addStatement("final $L $L = $L.executeUpdateDelete()",
- method.returnType.typeName(), resultVar, stmtName)
- addStatement("$N.setTransactionSuccessful()", dbField)
- addStatement("return $L", resultVar)
- } else {
- addStatement("$L.executeUpdateDelete()", stmtName)
- addStatement("$N.setTransactionSuccessful()", dbField)
- }
- }
- nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
- addStatement("$N.release($L)", preparedStmtField, stmtName)
- }
- endControlFlow()
- }
- return methodBuilder.build()
- }
-
- private fun createTransactionMethods(): List<PreparedStmtQuery> {
- return dao.transactionMethods.map {
- PreparedStmtQuery(emptyMap(), createTransactionMethodBody(it))
- }
- }
-
- private fun createTransactionMethodBody(method: TransactionMethod): MethodSpec {
- val scope = CodeGenScope(this)
- val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
- addStatement("$N.beginTransaction()", dbField)
- beginControlFlow("try").apply {
- val returnsValue = method.element.returnType.kind != TypeKind.VOID
- val resultVar = if (returnsValue) {
- scope.getTmpVar("_result")
- } else {
- null
- }
- addDelegateToSuperStatement(method.element, method.callType, resultVar)
- addStatement("$N.setTransactionSuccessful()", dbField)
- if (returnsValue) {
- addStatement("return $N", resultVar)
- }
- }
- nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
- }
- endControlFlow()
- }
- return methodBuilder.build()
- }
-
- private fun MethodSpec.Builder.addDelegateToSuperStatement(
- element: ExecutableElement,
- callType: TransactionMethod.CallType,
- result: String?) {
- val params: MutableList<Any> = mutableListOf()
- val format = buildString {
- if (result != null) {
- append("$T $L = ")
- params.add(element.returnType)
- params.add(result)
- }
- when (callType) {
- TransactionMethod.CallType.CONCRETE -> {
- append("super.$N(")
- params.add(element.simpleName)
- }
- TransactionMethod.CallType.DEFAULT_JAVA8 -> {
- append("$N.super.$N(")
- params.add(element.enclosingElement.simpleName)
- params.add(element.simpleName)
- }
- TransactionMethod.CallType.DEFAULT_KOTLIN -> {
- append("$N.$N.$N(this, ")
- params.add(element.enclosingElement.simpleName)
- params.add(JvmAbi.DEFAULT_IMPLS_CLASS_NAME)
- params.add(element.simpleName)
- }
- }
- var first = true
- element.parameters.forEach {
- if (first) {
- first = false
- } else {
- append(", ")
- }
- append(L)
- params.add(it.simpleName)
- }
- append(")")
- }
- addStatement(format, *params.toTypedArray())
- }
-
- private fun createConstructor(
- dbParam: ParameterSpec,
- shortcutMethods: List<PreparedStmtQuery>,
- callSuper: Boolean): MethodSpec {
- return MethodSpec.constructorBuilder().apply {
- addParameter(dbParam)
- addModifiers(PUBLIC)
- if (callSuper) {
- addStatement("super($N)", dbParam)
- }
- addStatement("this.$N = $N", dbField, dbParam)
- shortcutMethods.filterNot {
- it.fields.isEmpty()
- }.map {
- it.fields.values
- }.flatten().groupBy {
- it.first.name
- }.map {
- it.value.first()
- }.forEach {
- addStatement("this.$N = $L", it.first, it.second)
- }
- }.build()
- }
-
- private fun createSelectMethod(method: QueryMethod): MethodSpec {
- return overrideWithoutAnnotations(method.element, declaredDao).apply {
- addCode(createQueryMethodBody(method))
- }.build()
- }
-
- private fun createRawQueryMethod(method: RawQueryMethod): MethodSpec {
- return overrideWithoutAnnotations(method.element, declaredDao).apply {
- val scope = CodeGenScope(this@DaoWriter)
- val roomSQLiteQueryVar: String
- val queryParam = method.runtimeQueryParam
- val shouldReleaseQuery: Boolean
-
- when {
- queryParam?.isString() == true -> {
- roomSQLiteQueryVar = scope.getTmpVar("_statement")
- shouldReleaseQuery = true
- addStatement("$T $L = $T.acquire($L, 0)",
- RoomTypeNames.ROOM_SQL_QUERY,
- roomSQLiteQueryVar,
- RoomTypeNames.ROOM_SQL_QUERY,
- queryParam.paramName)
- }
- queryParam?.isSupportQuery() == true -> {
- shouldReleaseQuery = false
- roomSQLiteQueryVar = scope.getTmpVar("_internalQuery")
- // move it to a final variable so that the generated code can use it inside
- // callback blocks in java 7
- addStatement("final $T $L = $N",
- queryParam.type,
- roomSQLiteQueryVar,
- queryParam.paramName)
- }
- else -> {
- // try to generate compiling code. we would've already reported this error
- roomSQLiteQueryVar = scope.getTmpVar("_statement")
- shouldReleaseQuery = false
- addStatement("$T $L = $T.acquire($L, 0)",
- RoomTypeNames.ROOM_SQL_QUERY,
- roomSQLiteQueryVar,
- RoomTypeNames.ROOM_SQL_QUERY,
- "missing query parameter")
- }
- }
- if (method.returnsValue) {
- // don't generate code because it will create 1 more error. The original error is
- // already reported by the processor.
- method.queryResultBinder.convertAndReturn(
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- canReleaseQuery = shouldReleaseQuery,
- dbField = dbField,
- inTransaction = method.inTransaction,
- scope = scope)
- }
- addCode(scope.builder().build())
- }.build()
- }
-
- private fun createDeleteOrUpdateQueryMethod(method: QueryMethod): MethodSpec {
- return overrideWithoutAnnotations(method.element, declaredDao).apply {
- addCode(createDeleteOrUpdateQueryMethodBody(method))
- }.build()
- }
-
- /**
- * Groups all insertion methods based on the insert statement they will use then creates all
- * field specs, EntityInsertionAdapterWriter and actual insert methods.
- */
- private fun createInsertionMethods(): List<PreparedStmtQuery> {
- return dao.insertionMethods
- .map { insertionMethod ->
- val onConflict = OnConflictProcessor.onConflictText(insertionMethod.onConflict)
- val entities = insertionMethod.entities
-
- val fields = entities.mapValues {
- val spec = getOrCreateField(InsertionMethodField(it.value, onConflict))
- val impl = EntityInsertionAdapterWriter(it.value, onConflict)
- .createAnonymous(this@DaoWriter, dbField.name)
- spec to impl
- }
- val methodImpl = overrideWithoutAnnotations(insertionMethod.element,
- declaredDao).apply {
- addCode(createInsertionMethodBody(insertionMethod, fields))
- }.build()
- PreparedStmtQuery(fields, methodImpl)
- }.filterNotNull()
- }
-
- private fun createInsertionMethodBody(
- method: InsertionMethod,
- insertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>
- ): CodeBlock {
- val insertionType = method.insertionType
- if (insertionAdapters.isEmpty() || insertionType == null) {
- return CodeBlock.builder().build()
- }
- val scope = CodeGenScope(this)
-
- return scope.builder().apply {
- // TODO assert thread
- // TODO collect results
- addStatement("$N.beginTransaction()", dbField)
- val needsReturnType = insertionType != InsertionMethod.Type.INSERT_VOID
- val resultVar = if (needsReturnType) {
- scope.getTmpVar("_result")
- } else {
- null
- }
-
- beginControlFlow("try").apply {
- method.parameters.forEach { param ->
- val insertionAdapter = insertionAdapters[param.name]?.first
- if (needsReturnType) {
- // if it has more than 1 parameter, we would've already printed the error
- // so we don't care about re-declaring the variable here
- addStatement("$T $L = $N.$L($L)",
- insertionType.returnTypeName, resultVar,
- insertionAdapter, insertionType.methodName,
- param.name)
- } else {
- addStatement("$N.$L($L)", insertionAdapter, insertionType.methodName,
- param.name)
- }
- }
- addStatement("$N.setTransactionSuccessful()", dbField)
- if (needsReturnType) {
- addStatement("return $L", resultVar)
- }
- }
- nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
- }
- endControlFlow()
- }.build()
- }
-
- /**
- * Creates EntityUpdateAdapter for each deletion method.
- */
- private fun createDeletionMethods(): List<PreparedStmtQuery> {
- return createShortcutMethods(dao.deletionMethods, "deletion", { _, entity ->
- EntityDeletionAdapterWriter(entity)
- .createAnonymous(this@DaoWriter, dbField.name)
- })
- }
-
- /**
- * Creates EntityUpdateAdapter for each @Update method.
- */
- private fun createUpdateMethods(): List<PreparedStmtQuery> {
- return createShortcutMethods(dao.updateMethods, "update", { update, entity ->
- val onConflict = OnConflictProcessor.onConflictText(update.onConflictStrategy)
- EntityUpdateAdapterWriter(entity, onConflict)
- .createAnonymous(this@DaoWriter, dbField.name)
- })
- }
-
- private fun <T : ShortcutMethod> createShortcutMethods(
- methods: List<T>, methodPrefix: String,
- implCallback: (T, Entity) -> TypeSpec
- ): List<PreparedStmtQuery> {
- return methods.map { method ->
- val entities = method.entities
-
- if (entities.isEmpty()) {
- null
- } else {
- val fields = entities.mapValues {
- val spec = getOrCreateField(DeleteOrUpdateAdapterField(it.value, methodPrefix))
- val impl = implCallback(method, it.value)
- spec to impl
- }
- val methodSpec = overrideWithoutAnnotations(method.element, declaredDao).apply {
- addCode(createDeleteOrUpdateMethodBody(method, fields))
- }.build()
- PreparedStmtQuery(fields, methodSpec)
- }
- }.filterNotNull()
- }
-
- private fun createDeleteOrUpdateMethodBody(
- method: ShortcutMethod,
- adapters: Map<String, Pair<FieldSpec, TypeSpec>>
- ): CodeBlock {
- if (adapters.isEmpty()) {
- return CodeBlock.builder().build()
- }
- val scope = CodeGenScope(this)
- val resultVar = if (method.returnCount) {
- scope.getTmpVar("_total")
- } else {
- null
- }
- return scope.builder().apply {
- if (resultVar != null) {
- addStatement("$T $L = 0", TypeName.INT, resultVar)
- }
- addStatement("$N.beginTransaction()", dbField)
- beginControlFlow("try").apply {
- method.parameters.forEach { param ->
- val adapter = adapters[param.name]?.first
- addStatement("$L$N.$L($L)",
- if (resultVar == null) "" else "$resultVar +=",
- adapter, param.handleMethodName(), param.name)
- }
- addStatement("$N.setTransactionSuccessful()", dbField)
- if (resultVar != null) {
- addStatement("return $L", resultVar)
- }
- }
- nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
- }
- endControlFlow()
- }.build()
- }
-
- /**
- * @Query with delete action
- */
- private fun createDeleteOrUpdateQueryMethodBody(method: QueryMethod): CodeBlock {
- val queryWriter = QueryWriter(method)
- val scope = CodeGenScope(this)
- val sqlVar = scope.getTmpVar("_sql")
- val stmtVar = scope.getTmpVar("_stmt")
- val listSizeArgs = queryWriter.prepareQuery(sqlVar, scope)
- scope.builder().apply {
- addStatement("$T $L = $N.compileStatement($L)",
- SupportDbTypeNames.SQLITE_STMT, stmtVar, dbField, sqlVar)
- queryWriter.bindArgs(stmtVar, listSizeArgs, scope)
- addStatement("$N.beginTransaction()", dbField)
- beginControlFlow("try").apply {
- if (method.returnsValue) {
- val resultVar = scope.getTmpVar("_result")
- addStatement("final $L $L = $L.executeUpdateDelete()",
- method.returnType.typeName(), resultVar, stmtVar)
- addStatement("$N.setTransactionSuccessful()", dbField)
- addStatement("return $L", resultVar)
- } else {
- addStatement("$L.executeUpdateDelete()", stmtVar)
- addStatement("$N.setTransactionSuccessful()", dbField)
- }
- }
- nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
- }
- endControlFlow()
- }
- return scope.builder().build()
- }
-
- private fun createQueryMethodBody(method: QueryMethod): CodeBlock {
- val queryWriter = QueryWriter(method)
- val scope = CodeGenScope(this)
- val sqlVar = scope.getTmpVar("_sql")
- val roomSQLiteQueryVar = scope.getTmpVar("_statement")
- queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
- method.queryResultBinder.convertAndReturn(
- roomSQLiteQueryVar = roomSQLiteQueryVar,
- canReleaseQuery = true,
- dbField = dbField,
- inTransaction = method.inTransaction,
- scope = scope)
- return scope.builder().build()
- }
-
- private fun overrideWithoutAnnotations(
- elm: ExecutableElement,
- owner: DeclaredType): MethodSpec.Builder {
- val baseSpec = MethodSpec.overriding(elm, owner, processingEnv.typeUtils).build()
- return MethodSpec.methodBuilder(baseSpec.name).apply {
- addAnnotation(Override::class.java)
- addModifiers(baseSpec.modifiers)
- addParameters(baseSpec.parameters)
- varargs(baseSpec.varargs)
- returns(baseSpec.returnType)
- }
- }
-
- /**
- * Represents a query statement prepared in Dao implementation.
- *
- * @param fields This map holds all the member fields necessary for this query. The key is the
- * corresponding parameter name in the defining query method. The value is a pair from the field
- * declaration to definition.
- * @param methodImpl The body of the query method implementation.
- */
- data class PreparedStmtQuery(
- val fields: Map<String, Pair<FieldSpec, TypeSpec>>,
- val methodImpl: MethodSpec) {
- companion object {
- // The key to be used in `fields` where the method requires a field that is not
- // associated with any of its parameters
- const val NO_PARAM_FIELD = "-"
- }
- }
-
- private class InsertionMethodField(val entity: Entity, val onConflictText: String)
- : SharedFieldSpec(
- "insertionAdapterOf${Companion.typeNameToFieldName(entity.typeName)}",
- RoomTypeNames.INSERTION_ADAPTER) {
-
- override fun getUniqueKey(): String {
- return "${entity.typeName} $onConflictText"
- }
-
- override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
- builder.addModifiers(FINAL, PRIVATE)
- }
- }
-
- class DeleteOrUpdateAdapterField(val entity: Entity, val methodPrefix: String)
- : SharedFieldSpec(
- "${methodPrefix}AdapterOf${Companion.typeNameToFieldName(entity.typeName)}",
- RoomTypeNames.DELETE_OR_UPDATE_ADAPTER) {
- override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
- builder.addModifiers(PRIVATE, FINAL)
- }
-
- override fun getUniqueKey(): String {
- return entity.typeName.toString() + methodPrefix
- }
- }
-
- class PreparedStatementField(val method: QueryMethod) : SharedFieldSpec(
- "preparedStmtOf${method.name.capitalize()}", RoomTypeNames.SHARED_SQLITE_STMT) {
- override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
- builder.addModifiers(PRIVATE, FINAL)
- }
-
- override fun getUniqueKey(): String {
- return method.query.original
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt
deleted file mode 100644
index 7e58430..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DatabaseWriter.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.DaoMethod
-import android.arch.persistence.room.vo.Database
-import com.google.auto.common.MoreElements
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import stripNonJava
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PROTECTED
-import javax.lang.model.element.Modifier.PUBLIC
-import javax.lang.model.element.Modifier.VOLATILE
-
-/**
- * Writes implementation of classes that were annotated with @Database.
- */
-class DatabaseWriter(val database: Database) : ClassWriter(database.implTypeName) {
- override fun createTypeSpecBuilder(): TypeSpec.Builder {
- val builder = TypeSpec.classBuilder(database.implTypeName)
- builder.apply {
- addModifiers(PUBLIC)
- superclass(database.typeName)
- addMethod(createCreateOpenHelper())
- addMethod(createCreateInvalidationTracker())
- addMethod(createClearAllTables())
- }
- addDaoImpls(builder)
- return builder
- }
-
- private fun createClearAllTables(): MethodSpec {
- val scope = CodeGenScope(this)
- return MethodSpec.methodBuilder("clearAllTables").apply {
- val dbVar = scope.getTmpVar("_db")
- addStatement("final $T $L = super.getOpenHelper().getWritableDatabase()",
- SupportDbTypeNames.DB, dbVar)
- val deferVar = scope.getTmpVar("_supportsDeferForeignKeys")
- if (database.enableForeignKeys) {
- addStatement("boolean $L = $L.VERSION.SDK_INT >= $L.VERSION_CODES.LOLLIPOP",
- deferVar, AndroidTypeNames.BUILD, AndroidTypeNames.BUILD)
- }
- addAnnotation(Override::class.java)
- addModifiers(PUBLIC)
- returns(TypeName.VOID)
- beginControlFlow("try").apply {
- if (database.enableForeignKeys) {
- beginControlFlow("if (!$L)", deferVar).apply {
- addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = FALSE")
- }
- endControlFlow()
- }
- addStatement("super.beginTransaction()")
- if (database.enableForeignKeys) {
- beginControlFlow("if ($L)", deferVar).apply {
- addStatement("$L.execSQL($S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
- }
- endControlFlow()
- }
- database.entities.sortedWith(EntityDeleteComparator()).forEach {
- addStatement("$L.execSQL($S)", dbVar, "DELETE FROM `${it.tableName}`")
- }
- addStatement("super.setTransactionSuccessful()")
- }
- nextControlFlow("finally").apply {
- addStatement("super.endTransaction()")
- if (database.enableForeignKeys) {
- beginControlFlow("if (!$L)", deferVar).apply {
- addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = TRUE")
- }
- endControlFlow()
- }
- }
- endControlFlow()
- }.build()
- }
-
- private fun createCreateInvalidationTracker(): MethodSpec {
- return MethodSpec.methodBuilder("createInvalidationTracker").apply {
- addAnnotation(Override::class.java)
- addModifiers(PROTECTED)
- returns(RoomTypeNames.INVALIDATION_TRACKER)
- val tableNames = database.entities.joinToString(",") {
- "\"${it.tableName}\""
- }
- addStatement("return new $T(this, $L)", RoomTypeNames.INVALIDATION_TRACKER, tableNames)
- }.build()
- }
-
- private fun addDaoImpls(builder: TypeSpec.Builder) {
- val scope = CodeGenScope(this)
- builder.apply {
- database.daoMethods.forEach { method ->
- val name = method.dao.typeName.simpleName().decapitalize().stripNonJava()
- val fieldName = scope.getTmpVar("_$name")
- val field = FieldSpec.builder(method.dao.typeName, fieldName,
- PRIVATE, VOLATILE).build()
- addField(field)
- addMethod(createDaoGetter(field, method))
- }
- }
- }
-
- private fun createDaoGetter(field: FieldSpec, method: DaoMethod): MethodSpec {
- return MethodSpec.overriding(MoreElements.asExecutable(method.element)).apply {
- beginControlFlow("if ($N != null)", field).apply {
- addStatement("return $N", field)
- }
- nextControlFlow("else").apply {
- beginControlFlow("synchronized(this)").apply {
- beginControlFlow("if($N == null)", field).apply {
- addStatement("$N = new $T(this)", field, method.dao.implTypeName)
- }
- endControlFlow()
- addStatement("return $N", field)
- }
- endControlFlow()
- }
- endControlFlow()
- }.build()
- }
-
- private fun createCreateOpenHelper(): MethodSpec {
- val scope = CodeGenScope(this)
- return MethodSpec.methodBuilder("createOpenHelper").apply {
- addModifiers(Modifier.PROTECTED)
- addAnnotation(Override::class.java)
- returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)
-
- val configParam = ParameterSpec.builder(RoomTypeNames.ROOM_DB_CONFIG,
- "configuration").build()
- addParameter(configParam)
-
- val openHelperVar = scope.getTmpVar("_helper")
- val openHelperCode = scope.fork()
- SQLiteOpenHelperWriter(database)
- .write(openHelperVar, configParam, openHelperCode)
- addCode(openHelperCode.builder().build())
- addStatement("return $L", openHelperVar)
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt
deleted file mode 100644
index b9d6aec..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriter.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.FieldWithIndex
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeName
-import stripNonJava
-import javax.lang.model.element.Modifier.PRIVATE
-
-class EntityCursorConverterWriter(val entity: Entity) : ClassWriter.SharedMethodSpec(
- "entityCursorConverter_${entity.typeName.toString().stripNonJava()}") {
- override fun getUniqueKey(): String {
- return "generic_entity_converter_of_${entity.element.qualifiedName}"
- }
-
- override fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder) {
- builder.apply {
- val cursorParam = ParameterSpec
- .builder(AndroidTypeNames.CURSOR, "cursor").build()
- addParameter(cursorParam)
- addModifiers(PRIVATE)
- returns(entity.typeName)
- addCode(buildConvertMethodBody(writer, cursorParam))
- }
- }
-
- private fun depth(parent: EmbeddedField?): Int {
- return if (parent == null) {
- 0
- } else {
- 1 + depth(parent.parent)
- }
- }
-
- private fun buildConvertMethodBody(writer: ClassWriter, cursorParam: ParameterSpec): CodeBlock {
- val scope = CodeGenScope(writer)
- val entityVar = scope.getTmpVar("_entity")
- scope.builder().apply {
- scope.builder().addStatement("final $T $L", entity.typeName, entityVar)
- val fieldsWithIndices = entity.fields.map {
- val indexVar = scope.getTmpVar(
- "_cursorIndexOf${it.name.stripNonJava().capitalize()}")
- scope.builder().addStatement("final $T $L = $N.getColumnIndex($S)",
- TypeName.INT, indexVar, cursorParam, it.columnName)
- FieldWithIndex(field = it,
- indexVar = indexVar,
- alwaysExists = false)
- }
- FieldReadWriteWriter.readFromCursor(
- outVar = entityVar,
- outPojo = entity,
- cursorVar = cursorParam.name,
- fieldsWithIndices = fieldsWithIndices,
- relationCollectors = emptyList(), // no relationship for entities
- scope = scope)
- addStatement("return $L", entityVar)
- }
- return scope.builder().build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityDeleteComparator.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityDeleteComparator.kt
deleted file mode 100644
index a2bb737..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityDeleteComparator.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2018 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.writer
-
-import android.arch.persistence.room.vo.Entity
-
-/**
- * Sorts the entities by their foreign key dependencies. For example, when Entity A depends on
- * Entity B, A is ordered before B.
- */
-class EntityDeleteComparator : Comparator<Entity> {
-
- override fun compare(lhs: Entity, rhs: Entity): Int {
- val ltr = lhs.shouldBeDeletedAfter(rhs)
- val rtl = rhs.shouldBeDeletedAfter(lhs)
- return when {
- ltr == rtl -> 0
- ltr -> -1
- rtl -> 1
- else -> 0 // Never happens
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityDeletionAdapterWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityDeletionAdapterWriter.kt
deleted file mode 100644
index 29ba984..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityDeletionAdapterWriter.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.FieldWithIndex
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
-
-class EntityDeletionAdapterWriter(val entity: Entity) {
- fun createAnonymous(classWriter: ClassWriter, dbParam: String): TypeSpec {
- @Suppress("RemoveSingleExpressionStringTemplate")
- return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
- superclass(ParameterizedTypeName.get(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER,
- entity.typeName)
- )
- addMethod(MethodSpec.methodBuilder("createQuery").apply {
- addAnnotation(Override::class.java)
- returns(ClassName.get("java.lang", "String"))
- addModifiers(PUBLIC)
- val query = "DELETE FROM `${entity.tableName}` WHERE " +
- entity.primaryKey.fields.joinToString(" AND ") {
- "`${it.columnName}` = ?"
- }
- addStatement("return $S", query)
- }.build())
- addMethod(MethodSpec.methodBuilder("bind").apply {
- val bindScope = CodeGenScope(classWriter)
- addAnnotation(Override::class.java)
- val stmtParam = "stmt"
- addParameter(ParameterSpec.builder(SupportDbTypeNames.SQLITE_STMT,
- stmtParam).build())
- val valueParam = "value"
- addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
- returns(TypeName.VOID)
- addModifiers(PUBLIC)
- val mapped = FieldWithIndex.byOrder(entity.primaryKey.fields)
- FieldReadWriteWriter.bindToStatement(ownerVar = valueParam,
- stmtParamVar = stmtParam,
- fieldsWithIndices = mapped,
- scope = bindScope)
- addCode(bindScope.builder().build())
- }.build())
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityInsertionAdapterWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityInsertionAdapterWriter.kt
deleted file mode 100644
index 26ff47b..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityInsertionAdapterWriter.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.FieldWithIndex
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
-
-class EntityInsertionAdapterWriter(val entity: Entity, val onConflict: String) {
- fun createAnonymous(classWriter: ClassWriter, dbParam: String): TypeSpec {
- @Suppress("RemoveSingleExpressionStringTemplate")
- return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
- superclass(
- ParameterizedTypeName.get(RoomTypeNames.INSERTION_ADAPTER, entity.typeName)
- )
-
- // If there is an auto-increment primary key with primitive type, we consider 0 as
- // not set. For such fields, we must generate a slightly different insertion SQL.
- val primitiveAutoGenerateField = if (entity.primaryKey.autoGenerateId) {
- entity.primaryKey.fields.firstOrNull()?.let { field ->
- field.statementBinder?.typeMirror()?.let { binderType ->
- if (binderType.kind.isPrimitive) {
- field
- } else {
- null
- }
- }
- }
- } else {
- null
- }
- addMethod(MethodSpec.methodBuilder("createQuery").apply {
- addAnnotation(Override::class.java)
- returns(ClassName.get("java.lang", "String"))
- addModifiers(PUBLIC)
- val query =
- "INSERT OR $onConflict INTO `${entity.tableName}`(" +
- entity.fields.joinToString(",") {
- "`${it.columnName}`"
- } + ") VALUES (" +
- entity.fields.joinToString(",") {
- if (primitiveAutoGenerateField == it) {
- "nullif(?, 0)"
- } else {
- "?"
- }
- } + ")"
- addStatement("return $S", query)
- }.build())
- addMethod(MethodSpec.methodBuilder("bind").apply {
- val bindScope = CodeGenScope(classWriter)
- addAnnotation(Override::class.java)
- val stmtParam = "stmt"
- addParameter(ParameterSpec.builder(SupportDbTypeNames.SQLITE_STMT,
- stmtParam).build())
- val valueParam = "value"
- addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
- returns(TypeName.VOID)
- addModifiers(PUBLIC)
- val mapped = FieldWithIndex.byOrder(entity.fields)
- FieldReadWriteWriter.bindToStatement(
- ownerVar = valueParam,
- stmtParamVar = stmtParam,
- fieldsWithIndices = mapped,
- scope = bindScope
- )
- addCode(bindScope.builder().build())
- }.build())
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityUpdateAdapterWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityUpdateAdapterWriter.kt
deleted file mode 100644
index 8dee8e1..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/EntityUpdateAdapterWriter.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.FieldWithIndex
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
-
-class EntityUpdateAdapterWriter(val entity: Entity, val onConflict: String) {
- fun createAnonymous(classWriter: ClassWriter, dbParam: String): TypeSpec {
- @Suppress("RemoveSingleExpressionStringTemplate")
- return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
- superclass(ParameterizedTypeName.get(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER,
- entity.typeName)
- )
- addMethod(MethodSpec.methodBuilder("createQuery").apply {
- addAnnotation(Override::class.java)
- returns(ClassName.get("java.lang", "String"))
- addModifiers(PUBLIC)
- val query = "UPDATE OR $onConflict `${entity.tableName}` SET " +
- entity.fields.joinToString(",") { field ->
- "`${field.columnName}` = ?"
- } + " WHERE " + entity.primaryKey.fields.joinToString(" AND ") {
- "`${it.columnName}` = ?"
- }
- addStatement("return $S", query)
- }.build())
- addMethod(MethodSpec.methodBuilder("bind").apply {
- val bindScope = CodeGenScope(classWriter)
- addAnnotation(Override::class.java)
- val stmtParam = "stmt"
- addParameter(ParameterSpec.builder(SupportDbTypeNames.SQLITE_STMT,
- stmtParam).build())
- val valueParam = "value"
- addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
- returns(TypeName.VOID)
- addModifiers(PUBLIC)
- val mappedField = FieldWithIndex.byOrder(entity.fields)
- FieldReadWriteWriter.bindToStatement(
- ownerVar = valueParam,
- stmtParamVar = stmtParam,
- fieldsWithIndices = mappedField,
- scope = bindScope
- )
- val pkeyStart = entity.fields.size
- val mappedPrimaryKeys = entity.primaryKey.fields.mapIndexed { index, field ->
- FieldWithIndex(field = field,
- indexVar = "${pkeyStart + index + 1}",
- alwaysExists = true)
- }
- FieldReadWriteWriter.bindToStatement(
- ownerVar = valueParam,
- stmtParamVar = stmtParam,
- fieldsWithIndices = mappedPrimaryKeys,
- scope = bindScope
- )
- addCode(bindScope.builder().build())
- }.build())
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/FieldReadWriteWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/FieldReadWriteWriter.kt
deleted file mode 100644
index 4aa795a..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/FieldReadWriteWriter.kt
+++ /dev/null
@@ -1,378 +0,0 @@
-/*
- * 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.defaultValue
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.CallType
-import android.arch.persistence.room.vo.Constructor
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.FieldWithIndex
-import android.arch.persistence.room.vo.Pojo
-import android.arch.persistence.room.vo.RelationCollector
-import com.squareup.javapoet.TypeName
-
-/**
- * Handles writing a field into statement or reading it form statement.
- */
-class FieldReadWriteWriter(fieldWithIndex: FieldWithIndex) {
- val field = fieldWithIndex.field
- val indexVar = fieldWithIndex.indexVar
- val alwaysExists = fieldWithIndex.alwaysExists
-
- companion object {
- /*
- * Get all parents including the ones which have grand children in this list but does not
- * have any direct children in the list.
- */
- fun getAllParents(fields: List<Field>): Set<EmbeddedField> {
- val allParents = mutableSetOf<EmbeddedField>()
- fun addAllParents(field: Field) {
- var parent = field.parent
- while (parent != null) {
- if (allParents.add(parent)) {
- parent = parent.parent
- } else {
- break
- }
- }
- }
- fields.forEach(::addAllParents)
- return allParents
- }
-
- /**
- * Convert the fields with indices into a Node tree so that we can recursively process
- * them. This work is done here instead of parsing because the result may include arbitrary
- * fields.
- */
- private fun createNodeTree(
- rootVar: String,
- fieldsWithIndices: List<FieldWithIndex>,
- scope: CodeGenScope): Node {
- val allParents = getAllParents(fieldsWithIndices.map { it.field })
- val rootNode = Node(rootVar, null)
- rootNode.directFields = fieldsWithIndices.filter { it.field.parent == null }
- val parentNodes = allParents.associate {
- Pair(it, Node(
- varName = scope.getTmpVar("_tmp${it.field.name.capitalize()}"),
- fieldParent = it))
- }
- parentNodes.values.forEach { node ->
- val fieldParent = node.fieldParent!!
- val grandParent = fieldParent.parent
- val grandParentNode = grandParent?.let {
- parentNodes[it]
- } ?: rootNode
- node.directFields = fieldsWithIndices.filter { it.field.parent == fieldParent }
- node.parentNode = grandParentNode
- grandParentNode.subNodes.add(node)
- }
- return rootNode
- }
-
- fun bindToStatement(
- ownerVar: String,
- stmtParamVar: String,
- fieldsWithIndices: List<FieldWithIndex>,
- scope: CodeGenScope
- ) {
- fun visitNode(node: Node) {
- fun bindWithDescendants() {
- node.directFields.forEach {
- FieldReadWriteWriter(it).bindToStatement(
- ownerVar = node.varName,
- stmtParamVar = stmtParamVar,
- scope = scope
- )
- }
- node.subNodes.forEach(::visitNode)
- }
-
- val fieldParent = node.fieldParent
- if (fieldParent != null) {
- fieldParent.getter.writeGet(
- ownerVar = node.parentNode!!.varName,
- outVar = node.varName,
- builder = scope.builder()
- )
- scope.builder().apply {
- beginControlFlow("if($L != null)", node.varName).apply {
- bindWithDescendants()
- }
- nextControlFlow("else").apply {
- node.allFields().forEach {
- addStatement("$L.bindNull($L)", stmtParamVar, it.indexVar)
- }
- }
- endControlFlow()
- }
- } else {
- bindWithDescendants()
- }
- }
- visitNode(createNodeTree(ownerVar, fieldsWithIndices, scope))
- }
-
- /**
- * Just constructs the given item, does NOT DECLARE. Declaration happens outside the
- * reading statement since we may never read if the cursor does not have necessary
- * columns.
- */
- private fun construct(
- outVar: String,
- constructor: Constructor?,
- typeName: TypeName,
- localVariableNames: Map<String, FieldWithIndex>,
- localEmbeddeds: List<Node>, scope: CodeGenScope
- ) {
- if (constructor == null) {
- // best hope code generation
- scope.builder().apply {
- addStatement("$L = new $T()", outVar, typeName)
- }
- return
- }
- val variableNames = constructor.params.map { param ->
- when (param) {
- is Constructor.FieldParam -> localVariableNames.entries.firstOrNull {
- it.value.field === param.field
- }?.key
- is Constructor.EmbeddedParam -> localEmbeddeds.firstOrNull {
- it.fieldParent === param.embedded
- }?.varName
- else -> null
- }
- }
- val args = variableNames.joinToString(",") { it ?: "null" }
- scope.builder().apply {
- addStatement("$L = new $T($L)", outVar, typeName, args)
- }
- }
-
- /**
- * Reads the row into the given variable. It does not declare it but constructs it.
- */
- fun readFromCursor(
- outVar: String,
- outPojo: Pojo,
- cursorVar: String,
- fieldsWithIndices: List<FieldWithIndex>,
- scope: CodeGenScope,
- relationCollectors: List<RelationCollector>
- ) {
- fun visitNode(node: Node) {
- val fieldParent = node.fieldParent
- fun readNode() {
- // read constructor parameters into local fields
- val constructorFields = node.directFields.filter {
- it.field.setter.callType == CallType.CONSTRUCTOR
- }.associateBy { fwi ->
- FieldReadWriteWriter(fwi).readIntoTmpVar(cursorVar, scope)
- }
- // read decomposed fields
- node.subNodes.forEach(::visitNode)
- // construct the object
- if (fieldParent != null) {
- construct(outVar = node.varName,
- constructor = fieldParent.pojo.constructor,
- typeName = fieldParent.field.typeName,
- localEmbeddeds = node.subNodes,
- localVariableNames = constructorFields,
- scope = scope)
- } else {
- construct(outVar = node.varName,
- constructor = outPojo.constructor,
- typeName = outPojo.typeName,
- localEmbeddeds = node.subNodes,
- localVariableNames = constructorFields,
- scope = scope)
- }
- // ready any field that was not part of the constructor
- node.directFields.filterNot {
- it.field.setter.callType == CallType.CONSTRUCTOR
- }.forEach { fwi ->
- FieldReadWriteWriter(fwi).readFromCursor(
- ownerVar = node.varName,
- cursorVar = cursorVar,
- scope = scope)
- }
- // assign relationship fields which will be read later
- relationCollectors.filter { (relation) ->
- relation.field.parent === fieldParent
- }.forEach {
- it.writeReadParentKeyCode(
- cursorVarName = cursorVar,
- itemVar = node.varName,
- fieldsWithIndices = fieldsWithIndices,
- scope = scope)
- }
- // assign sub modes to fields if they were not part of the constructor.
- node.subNodes.map {
- val setter = it.fieldParent?.setter
- if (setter != null && setter.callType != CallType.CONSTRUCTOR) {
- Pair(it.varName, setter)
- } else {
- null
- }
- }.filterNotNull().forEach { (varName, setter) ->
- setter.writeSet(
- ownerVar = node.varName,
- inVar = varName,
- builder = scope.builder())
- }
- }
- if (fieldParent == null) {
- // root element
- // always declared by the caller so we don't declare this
- readNode()
- } else {
- // always declare, we'll set below
- scope.builder().addStatement("final $T $L", fieldParent.pojo.typeName,
- node.varName)
- if (fieldParent.nonNull) {
- readNode()
- } else {
- val myDescendants = node.allFields()
- val allNullCheck = myDescendants.joinToString(" && ") {
- if (it.alwaysExists) {
- "$cursorVar.isNull(${it.indexVar})"
- } else {
- "( ${it.indexVar} == -1 || $cursorVar.isNull(${it.indexVar}))"
- }
- }
- scope.builder().apply {
- beginControlFlow("if (! ($L))", allNullCheck).apply {
- readNode()
- }
- nextControlFlow(" else ").apply {
- addStatement("$L = null", node.varName)
- }
- endControlFlow()
- }
- }
- }
- }
- visitNode(createNodeTree(outVar, fieldsWithIndices, scope))
- }
- }
-
- /**
- * @param ownerVar The entity / pojo that owns this field. It must own this field! (not the
- * container pojo)
- * @param stmtParamVar The statement variable
- * @param scope The code generation scope
- */
- private fun bindToStatement(ownerVar: String, stmtParamVar: String, scope: CodeGenScope) {
- field.statementBinder?.let { binder ->
- val varName = if (field.getter.callType == CallType.FIELD) {
- "$ownerVar.${field.name}"
- } else {
- "$ownerVar.${field.getter.name}()"
- }
- binder.bindToStmt(stmtParamVar, indexVar, varName, scope)
- }
- }
-
- /**
- * @param ownerVar The entity / pojo that owns this field. It must own this field (not the
- * container pojo)
- * @param cursorVar The cursor variable
- * @param scope The code generation scope
- */
- private fun readFromCursor(ownerVar: String, cursorVar: String, scope: CodeGenScope) {
- fun toRead() {
- field.cursorValueReader?.let { reader ->
- scope.builder().apply {
- when (field.setter.callType) {
- CallType.FIELD -> {
- reader.readFromCursor("$ownerVar.${field.getter.name}", cursorVar,
- indexVar, scope)
- }
- CallType.METHOD -> {
- val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
- addStatement("final $T $L", field.getter.type.typeName(), tmpField)
- reader.readFromCursor(tmpField, cursorVar, indexVar, scope)
- addStatement("$L.$L($L)", ownerVar, field.setter.name, tmpField)
- }
- CallType.CONSTRUCTOR -> {
- // no-op
- }
- }
- }
- }
- }
- if (alwaysExists) {
- toRead()
- } else {
- scope.builder().apply {
- beginControlFlow("if ($L != -1)", indexVar).apply {
- toRead()
- }
- endControlFlow()
- }
- }
- }
-
- /**
- * Reads the value into a temporary local variable.
- */
- fun readIntoTmpVar(cursorVar: String, scope: CodeGenScope): String {
- val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
- val typeName = field.getter.type.typeName()
- scope.builder().apply {
- addStatement("final $T $L", typeName, tmpField)
- if (alwaysExists) {
- field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
- } else {
- beginControlFlow("if ($L == -1)", indexVar).apply {
- addStatement("$L = $L", tmpField, typeName.defaultValue())
- }
- nextControlFlow("else").apply {
- field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
- }
- endControlFlow()
- }
- }
- return tmpField
- }
-
- /**
- * On demand node which is created based on the fields that were passed into this class.
- */
- private class Node(
- // root for me
- val varName: String,
- // set if I'm a FieldParent
- val fieldParent: EmbeddedField?
- ) {
- // whom do i belong
- var parentNode: Node? = null
- // these fields are my direct fields
- lateinit var directFields: List<FieldWithIndex>
- // these nodes are under me
- val subNodes = mutableListOf<Node>()
-
- fun allFields(): List<FieldWithIndex> {
- return directFields + subNodes.flatMap { it.allFields() }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/PreparedStatementWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/PreparedStatementWriter.kt
deleted file mode 100644
index 7982ace..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/PreparedStatementWriter.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier
-
-/**
- * Creates anonymous classes for RoomTypeNames#SHARED_SQLITE_STMT.
- */
-class PreparedStatementWriter(val queryWriter: QueryWriter) {
- fun createAnonymous(classWriter: ClassWriter, dbParam: FieldSpec): TypeSpec {
- val scope = CodeGenScope(classWriter)
- @Suppress("RemoveSingleExpressionStringTemplate")
- return TypeSpec.anonymousClassBuilder("$N", dbParam).apply {
- superclass(RoomTypeNames.SHARED_SQLITE_STMT)
- addMethod(MethodSpec.methodBuilder("createQuery").apply {
- addAnnotation(Override::class.java)
- returns(ClassName.get("java.lang", "String"))
- addModifiers(Modifier.PUBLIC)
- val queryName = scope.getTmpVar("_query")
- val queryGenScope = scope.fork()
- queryWriter.prepareQuery(queryName, queryGenScope)
- addCode(queryGenScope.builder().build())
- addStatement("return $L", queryName)
- }.build())
- }.build()
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/QueryWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/QueryWriter.kt
deleted file mode 100644
index d99e989..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/QueryWriter.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.RoomTypeNames.ROOM_SQL_QUERY
-import android.arch.persistence.room.ext.RoomTypeNames.STRING_UTIL
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.ParsedQuery
-import android.arch.persistence.room.parser.Section
-import android.arch.persistence.room.parser.SectionType.BIND_VAR
-import android.arch.persistence.room.parser.SectionType.NEWLINE
-import android.arch.persistence.room.parser.SectionType.TEXT
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.QueryMethod
-import android.arch.persistence.room.vo.QueryParameter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeName
-
-/**
- * Writes the SQL query and arguments for a QueryMethod.
- */
-class QueryWriter constructor(val parameters: List<QueryParameter>,
- val sectionToParamMapping: List<Pair<Section, QueryParameter?>>,
- val query: ParsedQuery) {
-
- constructor(queryMethod: QueryMethod) : this(queryMethod.parameters,
- queryMethod.sectionToParamMapping, queryMethod.query) {
- }
-
- fun prepareReadAndBind(outSqlQueryName: String, outRoomSQLiteQueryVar: String,
- scope: CodeGenScope) {
- val listSizeVars = createSqlQueryAndArgs(outSqlQueryName, outRoomSQLiteQueryVar, scope)
- bindArgs(outRoomSQLiteQueryVar, listSizeVars, scope)
- }
-
- fun prepareQuery(
- outSqlQueryName: String, scope: CodeGenScope): List<Pair<QueryParameter, String>> {
- return createSqlQueryAndArgs(outSqlQueryName, null, scope)
- }
-
- private fun createSqlQueryAndArgs(
- outSqlQueryName: String,
- outArgsName: String?,
- scope: CodeGenScope
- ): List<Pair<QueryParameter, String>> {
- val listSizeVars = arrayListOf<Pair<QueryParameter, String>>()
- val varargParams = parameters
- .filter { it.queryParamAdapter?.isMultiple ?: false }
- val sectionToParamMapping = sectionToParamMapping
- val knownQueryArgsCount = sectionToParamMapping.filterNot {
- it.second?.queryParamAdapter?.isMultiple ?: false
- }.size
- scope.builder().apply {
- if (varargParams.isNotEmpty()) {
- val stringBuilderVar = scope.getTmpVar("_stringBuilder")
- addStatement("$T $L = $T.newStringBuilder()",
- ClassName.get(StringBuilder::class.java), stringBuilderVar, STRING_UTIL)
- query.sections.forEach {
- when (it.type) {
- TEXT -> addStatement("$L.append($S)", stringBuilderVar, it.text)
- NEWLINE -> addStatement("$L.append($S)", stringBuilderVar, "\n")
- BIND_VAR -> {
- // If it is null, will be reported as error before. We just try out
- // best to generate as much code as possible.
- sectionToParamMapping.firstOrNull { mapping ->
- mapping.first == it
- }?.let { pair ->
- if (pair.second?.queryParamAdapter?.isMultiple ?: false) {
- val tmpCount = scope.getTmpVar("_inputSize")
- listSizeVars.add(Pair(pair.second!!, tmpCount))
- pair.second
- ?.queryParamAdapter
- ?.getArgCount(pair.second!!.name, tmpCount, scope)
- addStatement("$T.appendPlaceholders($L, $L)",
- STRING_UTIL, stringBuilderVar, tmpCount)
- } else {
- addStatement("$L.append($S)", stringBuilderVar, "?")
- }
- }
- }
- }
- }
-
- addStatement("final $T $L = $L.toString()", String::class.typeName(),
- outSqlQueryName, stringBuilderVar)
- if (outArgsName != null) {
- val argCount = scope.getTmpVar("_argCount")
- addStatement("final $T $L = $L$L", TypeName.INT, argCount, knownQueryArgsCount,
- listSizeVars.joinToString("") { " + ${it.second}" })
- addStatement("final $T $L = $T.acquire($L, $L)",
- ROOM_SQL_QUERY, outArgsName, ROOM_SQL_QUERY, outSqlQueryName,
- argCount)
- }
- } else {
- addStatement("final $T $L = $S", String::class.typeName(),
- outSqlQueryName, query.queryWithReplacedBindParams)
- if (outArgsName != null) {
- addStatement("final $T $L = $T.acquire($L, $L)",
- ROOM_SQL_QUERY, outArgsName, ROOM_SQL_QUERY, outSqlQueryName,
- knownQueryArgsCount)
- }
- }
- }
- return listSizeVars
- }
-
- fun bindArgs(
- outArgsName: String,
- listSizeVars: List<Pair<QueryParameter, String>>,
- scope: CodeGenScope
- ) {
- if (parameters.isEmpty()) {
- return
- }
- scope.builder().apply {
- val argIndex = scope.getTmpVar("_argIndex")
- addStatement("$T $L = $L", TypeName.INT, argIndex, 1)
- // # of bindings with 1 placeholder
- var constInputs = 0
- // variable names for size of the bindings that have multiple args
- val varInputs = arrayListOf<String>()
- sectionToParamMapping.forEach { pair ->
- // reset the argIndex to the correct start index
- if (constInputs > 0 || varInputs.isNotEmpty()) {
- addStatement("$L = $L$L", argIndex,
- if (constInputs > 0) (1 + constInputs) else "1",
- varInputs.joinToString("") { " + $it" })
- }
- val param = pair.second
- param?.let {
- param.queryParamAdapter?.bindToStmt(param.name, outArgsName, argIndex, scope)
- }
- // add these to the list so that we can use them to calculate the next count.
- val sizeVar = listSizeVars.firstOrNull { it.first == param }
- if (sizeVar == null) {
- constInputs ++
- } else {
- varInputs.add(sizeVar.second)
- }
- }
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt
deleted file mode 100644
index ecd9b59..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/RelationCollectorMethodWriter.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * 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.writer
-
-import android.arch.persistence.room.ext.AndroidTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.RelationCollector
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import stripNonJava
-import javax.lang.model.element.Modifier
-
-/**
- * Writes the method that fetches the relations of a POJO and assigns them into the given map.
- */
-class RelationCollectorMethodWriter(private val collector: RelationCollector)
- : ClassWriter.SharedMethodSpec(
- "fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
- "As${collector.relation.pojoTypeName.toString().stripNonJava()}") {
- companion object {
- val KEY_SET_VARIABLE = "__mapKeySet"
- }
- override fun getUniqueKey(): String {
- val relation = collector.relation
- return "RelationCollectorMethodWriter" +
- "-${collector.mapTypeName}" +
- "-${relation.entity.typeName}" +
- "-${relation.entityField.columnName}" +
- "-${relation.pojoTypeName}" +
- "-${relation.createLoadAllSql()}"
- }
-
- override fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder) {
- val scope = CodeGenScope(writer)
- val relation = collector.relation
-
- val param = ParameterSpec.builder(collector.mapTypeName, "_map")
- .addModifiers(Modifier.FINAL)
- .build()
- val sqlQueryVar = scope.getTmpVar("_sql")
- val keySetVar = KEY_SET_VARIABLE
-
- val cursorVar = "_cursor"
- val itemKeyIndexVar = "_itemKeyIndex"
- val stmtVar = scope.getTmpVar("_stmt")
- scope.builder().apply {
-
- val keySetType = ParameterizedTypeName.get(
- ClassName.get(Set::class.java), collector.keyTypeName
- )
- addStatement("final $T $L = $N.keySet()", keySetType, keySetVar, param)
- beginControlFlow("if ($L.isEmpty())", keySetVar).apply {
- addStatement("return")
- }
- endControlFlow()
- addStatement("// check if the size is too big, if so divide")
- beginControlFlow("if($N.size() > $T.MAX_BIND_PARAMETER_CNT)",
- param, RoomTypeNames.ROOM_DB).apply {
- // divide it into chunks
- val tmpMapVar = scope.getTmpVar("_tmpInnerMap")
- addStatement("$T $L = new $T($L.MAX_BIND_PARAMETER_CNT)",
- collector.mapTypeName, tmpMapVar,
- collector.mapTypeName, RoomTypeNames.ROOM_DB)
- val mapIndexVar = scope.getTmpVar("_mapIndex")
- val tmpIndexVar = scope.getTmpVar("_tmpIndex")
- val limitVar = scope.getTmpVar("_limit")
- addStatement("$T $L = 0", TypeName.INT, mapIndexVar)
- addStatement("$T $L = 0", TypeName.INT, tmpIndexVar)
- addStatement("final $T $L = $N.size()", TypeName.INT, limitVar, param)
- beginControlFlow("while($L < $L)", mapIndexVar, limitVar).apply {
- addStatement("$L.put($N.keyAt($L), $N.valueAt($L))",
- tmpMapVar, param, mapIndexVar, param, mapIndexVar)
- addStatement("$L++", mapIndexVar)
- addStatement("$L++", tmpIndexVar)
- beginControlFlow("if($L == $T.MAX_BIND_PARAMETER_CNT)",
- tmpIndexVar, RoomTypeNames.ROOM_DB).apply {
- // recursively load that batch
- addStatement("$L($L)", methodName, tmpMapVar)
- // clear nukes the backing data hence we create a new one
- addStatement("$L = new $T($T.MAX_BIND_PARAMETER_CNT)",
- tmpMapVar, collector.mapTypeName, RoomTypeNames.ROOM_DB)
- addStatement("$L = 0", tmpIndexVar)
- }.endControlFlow()
- }.endControlFlow()
- beginControlFlow("if($L > 0)", tmpIndexVar).apply {
- // load the last batch
- addStatement("$L($L)", methodName, tmpMapVar)
- }.endControlFlow()
- addStatement("return")
- }.endControlFlow()
- collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
-
- addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
- DaoWriter.dbField, stmtVar)
-
- beginControlFlow("try").apply {
- addStatement("final $T $L = $L.getColumnIndex($S)",
- TypeName.INT, itemKeyIndexVar, cursorVar, relation.entityField.columnName)
-
- beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
- addStatement("return")
- }
- endControlFlow()
-
- collector.rowAdapter.onCursorReady(cursorVar, scope)
- val tmpVarName = scope.getTmpVar("_item")
- beginControlFlow("while($L.moveToNext())", cursorVar).apply {
- // read key from the cursor
- collector.readKey(
- cursorVarName = cursorVar,
- indexVar = itemKeyIndexVar,
- scope = scope
- ) { keyVar ->
- val collectionVar = scope.getTmpVar("_tmpCollection")
- addStatement("$T $L = $N.get($L)", collector.collectionTypeName,
- collectionVar, param, keyVar)
- beginControlFlow("if ($L != null)", collectionVar).apply {
- addStatement("final $T $L", relation.pojoTypeName, tmpVarName)
- collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
- addStatement("$L.add($L)", collectionVar, tmpVarName)
- }
- endControlFlow()
- }
- }
- endControlFlow()
- collector.rowAdapter.onCursorFinished()?.invoke(scope)
- }
- nextControlFlow("finally").apply {
- addStatement("$L.close()", cursorVar)
- }
- endControlFlow()
- }
- builder.apply {
- addModifiers(Modifier.PRIVATE)
- addParameter(param)
- returns(TypeName.VOID)
- addCode(scope.builder().build())
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
deleted file mode 100644
index 09d6d52..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriter.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Database
-import android.arch.persistence.room.vo.Entity
-import android.support.annotation.VisibleForTesting
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PROTECTED
-import javax.lang.model.element.Modifier.PUBLIC
-
-/**
- * Create an open helper using SupportSQLiteOpenHelperFactory
- */
-class SQLiteOpenHelperWriter(val database: Database) {
- fun write(outVar: String, configuration: ParameterSpec, scope: CodeGenScope) {
- scope.builder().apply {
- val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
- val callbackVar = scope.getTmpVar("_openCallback")
- addStatement("final $T $L = new $T($N, $L, $S, $S)",
- SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
- callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
- createOpenCallback(scope), database.identityHash, database.legacyIdentityHash)
- // build configuration
- addStatement(
- """
- final $T $L = $T.builder($N.context)
- .name($N.name)
- .callback($L)
- .build()
- """.trimIndent(),
- SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG, sqliteConfigVar,
- SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
- configuration, configuration, callbackVar)
- addStatement("final $T $N = $N.sqliteOpenHelperFactory.create($L)",
- SupportDbTypeNames.SQLITE_OPEN_HELPER, outVar,
- configuration, sqliteConfigVar)
- }
- }
-
- private fun createOpenCallback(scope: CodeGenScope): TypeSpec {
- return TypeSpec.anonymousClassBuilder(L, database.version).apply {
- superclass(RoomTypeNames.OPEN_HELPER_DELEGATE)
- addMethod(createCreateAllTables())
- addMethod(createDropAllTables())
- addMethod(createOnCreate(scope.fork()))
- addMethod(createOnOpen(scope.fork()))
- addMethod(createValidateMigration(scope.fork()))
- }.build()
- }
-
- private fun createValidateMigration(scope: CodeGenScope): MethodSpec {
- return MethodSpec.methodBuilder("validateMigration").apply {
- addModifiers(PROTECTED)
- addAnnotation(Override::class.java)
- val dbParam = ParameterSpec.builder(SupportDbTypeNames.DB, "_db").build()
- addParameter(dbParam)
- database.entities.forEach { entity ->
- val methodScope = scope.fork()
- TableInfoValidationWriter(entity).write(dbParam, methodScope)
- addCode(methodScope.builder().build())
- }
- }.build()
- }
-
- private fun createOnCreate(scope: CodeGenScope): MethodSpec {
- return MethodSpec.methodBuilder("onCreate").apply {
- addModifiers(PROTECTED)
- addAnnotation(Override::class.java)
- addParameter(SupportDbTypeNames.DB, "_db")
- invokeCallbacks(scope, "onCreate")
- }.build()
- }
-
- private fun createOnOpen(scope: CodeGenScope): MethodSpec {
- return MethodSpec.methodBuilder("onOpen").apply {
- addModifiers(PUBLIC)
- addAnnotation(Override::class.java)
- addParameter(SupportDbTypeNames.DB, "_db")
- addStatement("mDatabase = _db")
- if (database.enableForeignKeys) {
- addStatement("_db.execSQL($S)", "PRAGMA foreign_keys = ON")
- }
- addStatement("internalInitInvalidationTracker(_db)")
- invokeCallbacks(scope, "onOpen")
- }.build()
- }
-
- private fun createCreateAllTables(): MethodSpec {
- return MethodSpec.methodBuilder("createAllTables").apply {
- addModifiers(PUBLIC)
- addAnnotation(Override::class.java)
- addParameter(SupportDbTypeNames.DB, "_db")
- database.bundle.buildCreateQueries().forEach {
- addStatement("_db.execSQL($S)", it)
- }
- }.build()
- }
-
- private fun createDropAllTables(): MethodSpec {
- return MethodSpec.methodBuilder("dropAllTables").apply {
- addModifiers(PUBLIC)
- addAnnotation(Override::class.java)
- addParameter(SupportDbTypeNames.DB, "_db")
- database.entities.forEach {
- addStatement("_db.execSQL($S)", createDropTableQuery(it))
- }
- }.build()
- }
-
- private fun MethodSpec.Builder.invokeCallbacks(scope: CodeGenScope, methodName: String) {
- val iVar = scope.getTmpVar("_i")
- val sizeVar = scope.getTmpVar("_size")
- beginControlFlow("if (mCallbacks != null)").apply {
- beginControlFlow("for (int $N = 0, $N = mCallbacks.size(); $N < $N; $N++)",
- iVar, sizeVar, iVar, sizeVar, iVar).apply {
- addStatement("mCallbacks.get($N).$N(_db)", iVar, methodName)
- }
- endControlFlow()
- }
- endControlFlow()
- }
-
- @VisibleForTesting
- fun createQuery(entity: Entity): String {
- return entity.createTableQuery
- }
-
- @VisibleForTesting
- fun createDropTableQuery(entity: Entity): String {
- return "DROP TABLE IF EXISTS `${entity.tableName}`"
- }
-}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt
deleted file mode 100644
index 3c67475..0000000
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/TableInfoValidationWriter.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.writer
-
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.N
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.vo.Entity
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import stripNonJava
-import java.util.Arrays
-import java.util.HashMap
-import java.util.HashSet
-
-class TableInfoValidationWriter(val entity: Entity) {
- fun write(dbParam: ParameterSpec, scope: CodeGenScope) {
- val suffix = entity.tableName.stripNonJava().capitalize()
- val expectedInfoVar = scope.getTmpVar("_info$suffix")
- scope.builder().apply {
- val columnListVar = scope.getTmpVar("_columns$suffix")
- val columnListType = ParameterizedTypeName.get(HashMap::class.typeName(),
- CommonTypeNames.STRING, RoomTypeNames.TABLE_INFO_COLUMN)
-
- addStatement("final $T $L = new $T($L)", columnListType, columnListVar,
- columnListType, entity.fields.size)
- entity.fields.forEach { field ->
- addStatement("$L.put($S, new $T($S, $S, $L, $L))",
- columnListVar, field.columnName, RoomTypeNames.TABLE_INFO_COLUMN,
- /*name*/ field.columnName,
- /*type*/ field.affinity?.name ?: SQLTypeAffinity.TEXT.name,
- /*nonNull*/ field.nonNull,
- /*pkeyPos*/ entity.primaryKey.fields.indexOf(field) + 1)
- }
-
- val foreignKeySetVar = scope.getTmpVar("_foreignKeys$suffix")
- val foreignKeySetType = ParameterizedTypeName.get(HashSet::class.typeName(),
- RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
- addStatement("final $T $L = new $T($L)", foreignKeySetType, foreignKeySetVar,
- foreignKeySetType, entity.foreignKeys.size)
- entity.foreignKeys.forEach {
- val myColumnNames = it.childFields
- .joinToString(",") { "\"${it.columnName}\"" }
- val refColumnNames = it.parentColumns
- .joinToString(",") { "\"$it\"" }
- addStatement("$L.add(new $T($S, $S, $S," +
- "$T.asList($L), $T.asList($L)))", foreignKeySetVar,
- RoomTypeNames.TABLE_INFO_FOREIGN_KEY,
- /*parent table*/ it.parentTable,
- /*on delete*/ it.onDelete.sqlName,
- /*on update*/ it.onUpdate.sqlName,
- Arrays::class.typeName(),
- /*parent names*/ myColumnNames,
- Arrays::class.typeName(),
- /*parent column names*/ refColumnNames)
- }
-
- val indicesSetVar = scope.getTmpVar("_indices$suffix")
- val indicesType = ParameterizedTypeName.get(HashSet::class.typeName(),
- RoomTypeNames.TABLE_INFO_INDEX)
- addStatement("final $T $L = new $T($L)", indicesType, indicesSetVar,
- indicesType, entity.indices.size)
- entity.indices.forEach { index ->
- val columnNames = index.fields
- .joinToString(",") { "\"${it.columnName}\"" }
- addStatement("$L.add(new $T($S, $L, $T.asList($L)))",
- indicesSetVar,
- RoomTypeNames.TABLE_INFO_INDEX,
- index.name,
- index.unique,
- Arrays::class.typeName(),
- columnNames)
- }
-
- addStatement("final $T $L = new $T($S, $L, $L, $L)",
- RoomTypeNames.TABLE_INFO, expectedInfoVar, RoomTypeNames.TABLE_INFO,
- entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar)
-
- val existingVar = scope.getTmpVar("_existing$suffix")
- addStatement("final $T $L = $T.read($N, $S)",
- RoomTypeNames.TABLE_INFO, existingVar, RoomTypeNames.TABLE_INFO,
- dbParam, entity.tableName)
-
- beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
- addStatement("throw new $T($S + $L + $S + $L)",
- IllegalStateException::class.typeName(),
- "Migration didn't properly handle ${entity.tableName}" +
- "(${entity.element.qualifiedName}).\n Expected:\n",
- expectedInfoVar, "\n Found:\n", existingVar)
- }
- endControlFlow()
- }
- }
-}
diff --git a/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
new file mode 100644
index 0000000..eec5e9f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 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 androidx.room
+
+import androidx.room.processor.Context
+import androidx.room.processor.DatabaseProcessor
+import androidx.room.processor.ProcessorErrors
+import androidx.room.vo.DaoMethod
+import androidx.room.vo.Warning
+import androidx.room.writer.DaoWriter
+import androidx.room.writer.DatabaseWriter
+import com.google.auto.common.BasicAnnotationProcessor
+import com.google.auto.common.MoreElements
+import com.google.common.collect.SetMultimap
+import java.io.File
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.Element
+
+/**
+ * The annotation processor for Room.
+ */
+class RoomProcessor : BasicAnnotationProcessor() {
+ override fun initSteps(): MutableIterable<ProcessingStep>? {
+ val context = Context(processingEnv)
+ return arrayListOf(DatabaseProcessingStep(context))
+ }
+
+ override fun getSupportedOptions(): MutableSet<String> {
+ return Context.ARG_OPTIONS.toMutableSet()
+ }
+
+ override fun getSupportedSourceVersion(): SourceVersion {
+ return SourceVersion.latest()
+ }
+
+ class DatabaseProcessingStep(context: Context) : ContextBoundProcessingStep(context) {
+ override fun process(
+ elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>
+ ): MutableSet<Element> {
+ // TODO multi step support
+ val databases = elementsByAnnotation[Database::class.java]
+ ?.map {
+ DatabaseProcessor(context, MoreElements.asType(it)).process()
+ }
+ val allDaoMethods = databases?.flatMap { it.daoMethods }
+ allDaoMethods?.let {
+ prepareDaosForWriting(databases, it)
+ it.forEach {
+ DaoWriter(it.dao, context.processingEnv).write(context.processingEnv)
+ }
+ }
+
+ databases?.forEach { db ->
+ DatabaseWriter(db).write(context.processingEnv)
+ if (db.exportSchema) {
+ val schemaOutFolder = context.schemaOutFolder
+ if (schemaOutFolder == null) {
+ context.logger.w(Warning.MISSING_SCHEMA_LOCATION, db.element,
+ ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY)
+ } else {
+ if (!schemaOutFolder.exists()) {
+ schemaOutFolder.mkdirs()
+ }
+ val qName = db.element.qualifiedName.toString()
+ val dbSchemaFolder = File(schemaOutFolder, qName)
+ if (!dbSchemaFolder.exists()) {
+ dbSchemaFolder.mkdirs()
+ }
+ db.exportSchema(File(dbSchemaFolder, "${db.version}.json"))
+ }
+ }
+ }
+ return mutableSetOf()
+ }
+ override fun annotations(): MutableSet<out Class<out Annotation>> {
+ return mutableSetOf(Database::class.java, Dao::class.java, Entity::class.java)
+ }
+
+ /**
+ * Traverses all dao methods and assigns them suffix if they are used in multiple databases.
+ */
+ private fun prepareDaosForWriting(
+ databases: List<androidx.room.vo.Database>,
+ daoMethods: List<DaoMethod>) {
+ daoMethods.groupBy { it.dao.typeName }
+ // if used only in 1 database, nothing to do.
+ .filter { entry -> entry.value.size > 1 }
+ .forEach { entry ->
+ entry.value.groupBy { daoMethod ->
+ // first suffix guess: Database's simple name
+ val db = databases.first { db -> db.daoMethods.contains(daoMethod) }
+ db.typeName.simpleName()
+ }.forEach { (dbName, methods) ->
+ if (methods.size == 1) {
+ //good, db names do not clash, use db name as suffix
+ methods.first().dao.setSuffix(dbName)
+ } else {
+ // ok looks like a dao is used in 2 different databases both of
+ // which have the same name. enumerate.
+ methods.forEachIndexed { index, method ->
+ method.dao.setSuffix("${dbName}_$index")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ abstract class ContextBoundProcessingStep(val context: Context) : ProcessingStep
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/KotlinMetadataProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/ext/KotlinMetadataProcessor.kt
new file mode 100644
index 0000000..9177c23
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/KotlinMetadataProcessor.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 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 androidx.room.ext
+
+import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
+import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
+import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
+import org.jetbrains.kotlin.serialization.ProtoBuf
+import javax.lang.model.element.ExecutableElement
+
+/**
+ * Utility interface for processors that wants to run kotlin specific code.
+ */
+interface KotlinMetadataProcessor : KotlinMetadataUtils {
+ /**
+ * Returns the parameter names of the function if all have names embedded in the metadata.
+ */
+ fun KotlinClassMetadata.getParameterNames(method: ExecutableElement): List<String>? {
+ val valueParameterList = this.data.getFunctionOrNull(method)?.valueParameterList
+ ?: findConstructor(method)?.valueParameterList
+ ?: return null
+ return if (valueParameterList.all { it.hasName() }) {
+ valueParameterList.map {
+ data.nameResolver.getName(it.name)
+ .asString()
+ .replace("`", "")
+ .removeSuffix("?")
+ .trim()
+ }
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Finds the kotlin metadata for a constructor.
+ */
+ private fun KotlinClassMetadata.findConstructor(
+ executableElement: ExecutableElement
+ ): ProtoBuf.Constructor? {
+ val (nameResolver, classProto) = data
+ val jvmSignature = executableElement.jvmMethodSignature
+ // find constructor
+ return classProto.constructorList.singleOrNull {
+ it.getJvmConstructorSignature(nameResolver, classProto.typeTable) == jvmSignature
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
new file mode 100644
index 0000000..c1c6c17
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+
+package androidx.room.ext
+
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import org.jetbrains.kotlin.load.java.JvmAbi
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.type.WildcardType
+import javax.lang.model.util.SimpleAnnotationValueVisitor6
+import javax.lang.model.util.SimpleTypeVisitor7
+import javax.lang.model.util.Types
+import kotlin.reflect.KClass
+
+fun Element.hasAnyOf(vararg modifiers: Modifier): Boolean {
+ return this.modifiers.any { modifiers.contains(it) }
+}
+
+fun Element.hasAnnotation(klass: KClass<out Annotation>): Boolean {
+ return MoreElements.isAnnotationPresent(this, klass.java)
+}
+
+fun Element.isNonNull() =
+ asType().kind.isPrimitive
+ || hasAnnotation(androidx.annotation.NonNull::class)
+ || hasAnnotation(org.jetbrains.annotations.NotNull::class)
+
+/**
+ * gets all members including super privates. does not handle duplicate field names!!!
+ */
+// TODO handle conflicts with super: b/35568142
+fun TypeElement.getAllFieldsIncludingPrivateSupers(processingEnvironment: ProcessingEnvironment):
+ Set<VariableElement> {
+ val myMembers = processingEnvironment.elementUtils.getAllMembers(this)
+ .filter { it.kind == ElementKind.FIELD }
+ .filter { it is VariableElement }
+ .map { it as VariableElement }
+ .toSet()
+ if (superclass.kind != TypeKind.NONE) {
+ return myMembers + MoreTypes.asTypeElement(superclass)
+ .getAllFieldsIncludingPrivateSupers(processingEnvironment)
+ } else {
+ return myMembers
+ }
+}
+
+// code below taken from dagger2
+// compiler/src/main/java/dagger/internal/codegen/ConfigurationAnnotations.java
+private val TO_LIST_OF_TYPES = object
+ : SimpleAnnotationValueVisitor6<List<TypeMirror>, Void?>() {
+ override fun visitArray(values: MutableList<out AnnotationValue>?, p: Void?): List<TypeMirror> {
+ return values?.map {
+ val tmp = TO_TYPE.visit(it)
+ tmp
+ }?.filterNotNull() ?: emptyList()
+ }
+
+ override fun defaultAction(o: Any?, p: Void?): List<TypeMirror>? {
+ return emptyList()
+ }
+}
+
+private val TO_TYPE = object : SimpleAnnotationValueVisitor6<TypeMirror, Void>() {
+
+ override fun visitType(t: TypeMirror, p: Void?): TypeMirror {
+ return t
+ }
+
+ override fun defaultAction(o: Any?, p: Void?): TypeMirror {
+ throw TypeNotPresentException(o!!.toString(), null)
+ }
+}
+
+fun AnnotationValue.toListOfClassTypes(): List<TypeMirror> {
+ return TO_LIST_OF_TYPES.visit(this)
+}
+
+fun AnnotationValue.toType(): TypeMirror {
+ return TO_TYPE.visit(this)
+}
+
+fun AnnotationValue.toClassType(): TypeMirror? {
+ return TO_TYPE.visit(this)
+}
+
+fun TypeMirror.isCollection(): Boolean {
+ return MoreTypes.isType(this)
+ && (MoreTypes.isTypeOf(java.util.List::class.java, this)
+ || MoreTypes.isTypeOf(java.util.Set::class.java, this))
+}
+
+fun Element.getAnnotationValue(annotation: Class<out Annotation>, fieldName: String): Any? {
+ return MoreElements.getAnnotationMirror(this, annotation)
+ .orNull()?.let {
+ AnnotationMirrors.getAnnotationValue(it, fieldName)?.value
+ }
+}
+
+private val ANNOTATION_VALUE_TO_INT_VISITOR = object : SimpleAnnotationValueVisitor6<Int?, Void>() {
+ override fun visitInt(i: Int, p: Void?): Int? {
+ return i
+ }
+}
+
+private val ANNOTATION_VALUE_TO_BOOLEAN_VISITOR = object
+ : SimpleAnnotationValueVisitor6<Boolean?, Void>() {
+ override fun visitBoolean(b: Boolean, p: Void?): Boolean? {
+ return b
+ }
+}
+
+private val ANNOTATION_VALUE_TO_STRING_VISITOR = object
+ : SimpleAnnotationValueVisitor6<String?, Void>() {
+ override fun visitString(s: String?, p: Void?): String? {
+ return s
+ }
+}
+
+private val ANNOTATION_VALUE_STRING_ARR_VISITOR = object
+ : SimpleAnnotationValueVisitor6<List<String>, Void>() {
+ override fun visitArray(vals: MutableList<out AnnotationValue>?, p: Void?): List<String> {
+ return vals?.map {
+ ANNOTATION_VALUE_TO_STRING_VISITOR.visit(it)
+ }?.filterNotNull() ?: emptyList()
+ }
+}
+
+fun AnnotationValue.getAsInt(def: Int? = null): Int? {
+ return ANNOTATION_VALUE_TO_INT_VISITOR.visit(this) ?: def
+}
+
+fun AnnotationValue.getAsString(def: String? = null): String? {
+ return ANNOTATION_VALUE_TO_STRING_VISITOR.visit(this) ?: def
+}
+
+fun AnnotationValue.getAsBoolean(def: Boolean): Boolean {
+ return ANNOTATION_VALUE_TO_BOOLEAN_VISITOR.visit(this) ?: def
+}
+
+fun AnnotationValue.getAsStringList(): List<String> {
+ return ANNOTATION_VALUE_STRING_ARR_VISITOR.visit(this)
+}
+
+// a variant of Types.isAssignable that ignores variance.
+fun Types.isAssignableWithoutVariance(from: TypeMirror, to: TypeMirror): Boolean {
+ val assignable = isAssignable(from, to)
+ if (assignable) {
+ return true
+ }
+ if (from.kind != TypeKind.DECLARED || to.kind != TypeKind.DECLARED) {
+ return false
+ }
+ val declaredFrom = MoreTypes.asDeclared(from)
+ val declaredTo = MoreTypes.asDeclared(to)
+ val fromTypeArgs = declaredFrom.typeArguments
+ val toTypeArgs = declaredTo.typeArguments
+ // no type arguments, we don't need extra checks
+ if (fromTypeArgs.isEmpty() || fromTypeArgs.size != toTypeArgs.size) {
+ return false
+ }
+ // check erasure version first, if it does not match, no reason to proceed
+ if (!isAssignable(erasure(from), erasure(to))) {
+ return false
+ }
+ // convert from args to their upper bounds if it exists
+ val fromExtendsBounds = fromTypeArgs.map {
+ it.extendsBound()
+ }
+ // if there are no upper bound conversions, return.
+ if (fromExtendsBounds.all { it == null }) {
+ return false
+ }
+ // try to move the types of the from to their upper bounds. It does not matter for the "to"
+ // because Types.isAssignable handles it as it is valid java
+ return (0 until fromTypeArgs.size).all { index ->
+ isAssignableWithoutVariance(
+ from = fromExtendsBounds[index] ?: fromTypeArgs[index],
+ to = toTypeArgs[index])
+ }
+}
+
+// converts ? in Set< ? extends Foo> to Foo
+fun TypeMirror.extendsBound(): TypeMirror? {
+ return this.accept(object : SimpleTypeVisitor7<TypeMirror?, Void?>() {
+ override fun visitWildcard(type: WildcardType, ignored: Void?): TypeMirror? {
+ return type.extendsBound
+ }
+ }, null)
+}
+
+/**
+ * Finds the default implementation method corresponding to this Kotlin interface method.
+ */
+fun Element.findKotlinDefaultImpl(typeUtils: Types): Element? {
+ fun paramsMatch(ourParams: List<VariableElement>, theirParams: List<VariableElement>): Boolean {
+ if (ourParams.size != theirParams.size - 1) {
+ return false
+ }
+ ourParams.forEachIndexed { i, variableElement ->
+ // Plus 1 to their index because their first param is a self object.
+ if (!typeUtils.isSameType(theirParams[i + 1].asType(), variableElement.asType())) {
+ return false
+ }
+ }
+ return true
+ }
+
+ val parent = this.enclosingElement as TypeElement
+ val innerClass = parent.enclosedElements.find {
+ it.kind == ElementKind.CLASS && it.simpleName.contentEquals(JvmAbi.DEFAULT_IMPLS_CLASS_NAME)
+ } ?: return null
+ return innerClass.enclosedElements.find {
+ it.kind == ElementKind.METHOD && it.simpleName == this.simpleName
+ && paramsMatch(MoreElements.asExecutable(this).parameters,
+ MoreElements.asExecutable(it).parameters)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
new file mode 100644
index 0000000..5cf3476
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.ext
+
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
+import kotlin.reflect.KClass
+
+val L = "\$L"
+val T = "\$T"
+val N = "\$N"
+val S = "\$S"
+
+fun KClass<*>.typeName() = ClassName.get(this.java)
+fun KClass<*>.arrayTypeName() = ArrayTypeName.of(typeName())
+fun TypeMirror.typeName() = TypeName.get(this)
+
+object SupportDbTypeNames {
+ val DB: ClassName = ClassName.get("androidx.sqlite.db", "SupportSQLiteDatabase")
+ val SQLITE_STMT: ClassName =
+ ClassName.get("androidx.sqlite.db", "SupportSQLiteStatement")
+ val SQLITE_OPEN_HELPER: ClassName =
+ ClassName.get("androidx.sqlite.db", "SupportSQLiteOpenHelper")
+ val SQLITE_OPEN_HELPER_CALLBACK: ClassName =
+ ClassName.get("androidx.sqlite.db", "SupportSQLiteOpenHelper.Callback")
+ val SQLITE_OPEN_HELPER_FACTORY: ClassName =
+ ClassName.get("androidx.sqlite.db", "SupportSQLiteOpenHelper.Factory")
+ val SQLITE_OPEN_HELPER_CONFIG: ClassName =
+ ClassName.get("androidx.sqlite.db", "SupportSQLiteOpenHelper.Configuration")
+ val SQLITE_OPEN_HELPER_CONFIG_BUILDER: ClassName =
+ ClassName.get("androidx.sqlite.db",
+ "SupportSQLiteOpenHelper.Configuration.Builder")
+ val QUERY: ClassName =
+ ClassName.get("androidx.sqlite.db", "SupportSQLiteQuery")
+}
+
+object RoomTypeNames {
+ val STRING_UTIL: ClassName = ClassName.get("androidx.room.util", "StringUtil")
+ val CURSOR_CONVERTER: ClassName =
+ ClassName.get("androidx.room", "CursorConverter")
+ val ROOM: ClassName = ClassName.get("androidx.room", "Room")
+ val ROOM_DB: ClassName = ClassName.get("androidx.room", "RoomDatabase")
+ val ROOM_DB_CONFIG: ClassName = ClassName.get("androidx.room",
+ "DatabaseConfiguration")
+ val INSERTION_ADAPTER: ClassName =
+ ClassName.get("androidx.room", "EntityInsertionAdapter")
+ val DELETE_OR_UPDATE_ADAPTER: ClassName =
+ ClassName.get("androidx.room", "EntityDeletionOrUpdateAdapter")
+ val SHARED_SQLITE_STMT: ClassName =
+ ClassName.get("androidx.room", "SharedSQLiteStatement")
+ val INVALIDATION_TRACKER: ClassName =
+ ClassName.get("androidx.room", "InvalidationTracker")
+ val INVALIDATION_OBSERVER: ClassName =
+ ClassName.get("androidx.room.InvalidationTracker", "Observer")
+ val ROOM_SQL_QUERY: ClassName =
+ ClassName.get("androidx.room", "RoomSQLiteQuery")
+ val OPEN_HELPER: ClassName =
+ ClassName.get("androidx.room", "RoomOpenHelper")
+ val OPEN_HELPER_DELEGATE: ClassName =
+ ClassName.get("androidx.room", "RoomOpenHelper.Delegate")
+ val TABLE_INFO: ClassName =
+ ClassName.get("androidx.room.util", "TableInfo")
+ val TABLE_INFO_COLUMN: ClassName =
+ ClassName.get("androidx.room.util", "TableInfo.Column")
+ val TABLE_INFO_FOREIGN_KEY: ClassName =
+ ClassName.get("androidx.room.util", "TableInfo.ForeignKey")
+ val TABLE_INFO_INDEX: ClassName =
+ ClassName.get("androidx.room.util", "TableInfo.Index")
+ val LIMIT_OFFSET_DATA_SOURCE: ClassName =
+ ClassName.get("androidx.room.paging", "LimitOffsetDataSource")
+}
+
+object ArchTypeNames {
+ val APP_EXECUTOR: ClassName =
+ ClassName.get("androidx.arch.core.executor", "ArchTaskExecutor")
+}
+
+object PagingTypeNames {
+ val DATA_SOURCE: ClassName =
+ ClassName.get("androidx.paging", "DataSource")
+ val POSITIONAL_DATA_SOURCE: ClassName =
+ ClassName.get("androidx.paging", "PositionalDataSource")
+ val DATA_SOURCE_FACTORY: ClassName =
+ ClassName.get("androidx.paging", "DataSource.Factory")
+}
+
+object LifecyclesTypeNames {
+ val LIVE_DATA: ClassName = ClassName.get("androidx.lifecycle", "LiveData")
+ val COMPUTABLE_LIVE_DATA: ClassName = ClassName.get("androidx.lifecycle",
+ "ComputableLiveData")
+}
+
+object AndroidTypeNames {
+ val CURSOR: ClassName = ClassName.get("android.database", "Cursor")
+ val ARRAY_MAP: ClassName = ClassName.get("androidx.collection", "ArrayMap")
+ val BUILD: ClassName = ClassName.get("android.os", "Build")
+}
+
+object CommonTypeNames {
+ val LIST = ClassName.get("java.util", "List")
+ val SET = ClassName.get("java.util", "Set")
+ val STRING = ClassName.get("java.lang", "String")
+ val INTEGER = ClassName.get("java.lang", "Integer")
+ val OPTIONAL = ClassName.get("java.util", "Optional")
+}
+
+object GuavaBaseTypeNames {
+ val OPTIONAL = ClassName.get("com.google.common.base", "Optional")
+}
+
+object GuavaUtilConcurrentTypeNames {
+ val LISTENABLE_FUTURE = ClassName.get("com.google.common.util.concurrent", "ListenableFuture")
+}
+
+object RxJava2TypeNames {
+ val FLOWABLE = ClassName.get("io.reactivex", "Flowable")
+ val MAYBE = ClassName.get("io.reactivex", "Maybe")
+ val SINGLE = ClassName.get("io.reactivex", "Single")
+}
+
+object ReactiveStreamsTypeNames {
+ val PUBLISHER = ClassName.get("org.reactivestreams", "Publisher")
+}
+
+object RoomGuavaTypeNames {
+ val GUAVA_ROOM = ClassName.get("androidx.room.guava", "GuavaRoom")
+}
+
+object RoomRxJava2TypeNames {
+ val RX_ROOM = ClassName.get("androidx.room", "RxRoom")
+ val RX_EMPTY_RESULT_SET_EXCEPTION = ClassName.get("androidx.room",
+ "EmptyResultSetException")
+}
+
+fun TypeName.defaultValue(): String {
+ return if (!isPrimitive) {
+ "null"
+ } else if (this == TypeName.BOOLEAN) {
+ "false"
+ } else {
+ "0"
+ }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/string_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/string_ext.kt
similarity index 100%
rename from room/compiler/src/main/kotlin/android/arch/persistence/room/ext/string_ext.kt
rename to room/compiler/src/main/kotlin/androidx/room/ext/string_ext.kt
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/type_mirror_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/type_mirror_ext.kt
similarity index 100%
rename from room/compiler/src/main/kotlin/android/arch/persistence/room/ext/type_mirror_ext.kt
rename to room/compiler/src/main/kotlin/androidx/room/ext/type_mirror_ext.kt
diff --git a/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt b/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
new file mode 100644
index 0000000..22dd921
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+@file:Suppress("unused")
+
+package androidx.room.log
+
+import androidx.room.vo.Warning
+import java.util.UnknownFormatConversionException
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.tools.Diagnostic
+import javax.tools.Diagnostic.Kind.ERROR
+import javax.tools.Diagnostic.Kind.NOTE
+import javax.tools.Diagnostic.Kind.WARNING
+
+class RLog(val messager: Messager, val suppressedWarnings: Set<Warning>,
+ val defaultElement: Element?) {
+ private fun String.safeFormat(vararg args: Any): String {
+ try {
+ return format(args)
+ } catch (ex: UnknownFormatConversionException) {
+ // the input string might be from random source in which case we rather print the
+ // msg as is instead of crashing while reporting an error.
+ return this
+ }
+ }
+
+ fun d(element: Element, msg: String, vararg args: Any) {
+ messager.printMessage(NOTE, msg.safeFormat(args), element)
+ }
+
+ fun d(msg: String, vararg args: Any) {
+ messager.printMessage(NOTE, msg.safeFormat(args))
+ }
+
+ fun e(element: Element, msg: String, vararg args: Any) {
+ messager.printMessage(ERROR, msg.safeFormat(args), element)
+ }
+
+ fun e(msg: String, vararg args: Any) {
+ messager.printMessage(ERROR, msg.safeFormat(args), defaultElement)
+ }
+
+ fun w(warning: Warning, element: Element? = null, msg: String, vararg args: Any) {
+ if (suppressedWarnings.contains(warning)) {
+ return
+ }
+ messager.printMessage(WARNING, msg.safeFormat(args),
+ element ?: defaultElement)
+ }
+
+ fun w(warning: Warning, msg: String, vararg args: Any) {
+ if (suppressedWarnings.contains(warning)) {
+ return
+ }
+ messager.printMessage(WARNING, msg.safeFormat(args), defaultElement)
+ }
+
+ interface Messager {
+ fun printMessage(kind: Diagnostic.Kind, msg: String, element: Element? = null)
+ }
+
+ class ProcessingEnvMessager(val processingEnv: ProcessingEnvironment) : Messager {
+ override fun printMessage(kind: Diagnostic.Kind, msg: String, element: Element?) {
+ processingEnv.messager.printMessage(kind, msg, element)
+ }
+ }
+
+ class CollectingMessager : Messager {
+ private val messages = mutableMapOf<Diagnostic.Kind, MutableList<Pair<String, Element?>>> ()
+ override fun printMessage(kind: Diagnostic.Kind, msg: String, element: Element?) {
+ messages.getOrPut(kind, {
+ arrayListOf<Pair<String, Element?>>()
+ }).add(Pair(msg, element))
+ }
+
+ fun hasErrors() = messages.containsKey(Diagnostic.Kind.ERROR)
+
+ fun writeTo(env: ProcessingEnvironment) {
+ messages.forEach { pair ->
+ val kind = pair.key
+ pair.value.forEach {
+ env.messager.printMessage(kind, it.first, it.second)
+ }
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt b/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
new file mode 100644
index 0000000..b349012
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/ParsedQuery.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.parser
+
+import androidx.room.parser.SectionType.BIND_VAR
+import androidx.room.parser.SectionType.NEWLINE
+import androidx.room.parser.SectionType.TEXT
+import androidx.room.verifier.QueryResultInfo
+import org.antlr.v4.runtime.tree.TerminalNode
+
+enum class SectionType {
+ BIND_VAR,
+ TEXT,
+ NEWLINE
+}
+
+data class Section(val text: String, val type: SectionType) {
+ companion object {
+ fun text(text: String) = Section(text, SectionType.TEXT)
+ fun newline() = Section("", SectionType.NEWLINE)
+ fun bindVar(text: String) = Section(text, SectionType.BIND_VAR)
+ }
+}
+
+data class Table(val name: String, val alias: String)
+
+data class ParsedQuery(
+ val original: String,
+ val type: QueryType,
+ val inputs: List<TerminalNode>,
+ // pairs of table name and alias,
+ val tables: Set<Table>,
+ val syntaxErrors: List<String>,
+ val runtimeQueryPlaceholder: Boolean) {
+ companion object {
+ val STARTS_WITH_NUMBER = "^\\?[0-9]".toRegex()
+ val MISSING = ParsedQuery("missing query", QueryType.UNKNOWN, emptyList(), emptySet(),
+ emptyList(), false)
+ }
+
+ /**
+ * Optional data that might be assigned when the query is parsed inside an annotation processor.
+ * User may turn this off or it might be disabled for any reason so generated code should
+ * always handle not having it.
+ */
+ var resultInfo: QueryResultInfo? = null
+
+ val sections by lazy {
+ val lines = original.lines()
+ val inputsByLine = inputs.groupBy { it.symbol.line }
+ val sections = arrayListOf<Section>()
+ lines.forEachIndexed { index, line ->
+ var charInLine = 0
+ inputsByLine[index + 1]?.forEach { bindVar ->
+ if (charInLine < bindVar.symbol.charPositionInLine) {
+ sections.add(Section.text(line.substring(charInLine,
+ bindVar.symbol.charPositionInLine)))
+ }
+ sections.add(Section.bindVar(bindVar.text))
+ charInLine = bindVar.symbol.charPositionInLine + bindVar.symbol.text.length
+ }
+ if (charInLine < line.length) {
+ sections.add(Section.text(line.substring(charInLine)))
+ }
+ if (index + 1 < lines.size) {
+ sections.add(Section.newline())
+ }
+ }
+ sections
+ }
+
+ val bindSections by lazy { sections.filter { it.type == BIND_VAR } }
+
+ private fun unnamedVariableErrors(): List<String> {
+ val anonymousBindError = if (inputs.any { it.text == "?" }) {
+ arrayListOf(ParserErrors.ANONYMOUS_BIND_ARGUMENT)
+ } else {
+ emptyList<String>()
+ }
+ return anonymousBindError + inputs.filter {
+ it.text.matches(STARTS_WITH_NUMBER)
+ }.map {
+ ParserErrors.cannotUseVariableIndices(it.text, it.symbol.charPositionInLine)
+ }
+ }
+
+ private fun unknownQueryTypeErrors(): List<String> {
+ return if (QueryType.SUPPORTED.contains(type)) {
+ emptyList()
+ } else {
+ listOf(ParserErrors.invalidQueryType(type))
+ }
+ }
+
+ val errors by lazy {
+ if (syntaxErrors.isNotEmpty()) {
+ // if there is a syntax error, don't report others since they might be misleading.
+ syntaxErrors
+ } else {
+ unnamedVariableErrors() + unknownQueryTypeErrors()
+ }
+ }
+
+ val queryWithReplacedBindParams by lazy {
+ sections.joinToString("") {
+ when (it.type) {
+ TEXT -> it.text
+ BIND_VAR -> "?"
+ NEWLINE -> "\n"
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/ParserErrors.kt b/room/compiler/src/main/kotlin/androidx/room/parser/ParserErrors.kt
new file mode 100644
index 0000000..9c9d921
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/ParserErrors.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.parser
+
+object ParserErrors {
+ val ANONYMOUS_BIND_ARGUMENT = "Room does not support ? as bind parameters. You must use" +
+ " named bind arguments (e..g :argName)"
+
+ val NOT_ONE_QUERY = "Must have exactly 1 query in @Query value"
+
+ fun invalidQueryType(type: QueryType): String {
+ return "$type query type is not supported yet. You can use:" +
+ QueryType.SUPPORTED.joinToString(", ") { it.name }
+ }
+
+ fun cannotUseVariableIndices(name: String, position: Int) = "Cannot use variable indices." +
+ " Use named parameters instead (e.g. WHERE name LIKE :nameArg and lastName LIKE " +
+ ":lastName). Problem: $name at $position"
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
new file mode 100644
index 0000000..45b58ae
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.parser
+
+import androidx.room.ColumnInfo
+import org.antlr.v4.runtime.ANTLRInputStream
+import org.antlr.v4.runtime.BaseErrorListener
+import org.antlr.v4.runtime.CommonTokenStream
+import org.antlr.v4.runtime.RecognitionException
+import org.antlr.v4.runtime.Recognizer
+import org.antlr.v4.runtime.tree.ParseTree
+import org.antlr.v4.runtime.tree.TerminalNode
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@Suppress("FunctionName")
+class QueryVisitor(
+ private val original: String,
+ private val syntaxErrors: ArrayList<String>,
+ statement: ParseTree,
+ private val forRuntimeQuery: Boolean
+) : SQLiteBaseVisitor<Void?>() {
+ private val bindingExpressions = arrayListOf<TerminalNode>()
+ // table name alias mappings
+ private val tableNames = mutableSetOf<Table>()
+ private val withClauseNames = mutableSetOf<String>()
+ private val queryType: QueryType
+
+ init {
+ queryType = (0 until statement.childCount).map {
+ findQueryType(statement.getChild(it))
+ }.filterNot { it == QueryType.UNKNOWN }.firstOrNull() ?: QueryType.UNKNOWN
+
+ statement.accept(this)
+ }
+
+ private fun findQueryType(statement: ParseTree): QueryType {
+ return when (statement) {
+ is SQLiteParser.Factored_select_stmtContext,
+ is SQLiteParser.Compound_select_stmtContext,
+ is SQLiteParser.Select_stmtContext,
+ is SQLiteParser.Simple_select_stmtContext ->
+ QueryType.SELECT
+
+ is SQLiteParser.Delete_stmt_limitedContext,
+ is SQLiteParser.Delete_stmtContext ->
+ QueryType.DELETE
+
+ is SQLiteParser.Insert_stmtContext ->
+ QueryType.INSERT
+ is SQLiteParser.Update_stmtContext,
+ is SQLiteParser.Update_stmt_limitedContext ->
+ QueryType.UPDATE
+ is TerminalNode -> when (statement.text) {
+ "EXPLAIN" -> QueryType.EXPLAIN
+ else -> QueryType.UNKNOWN
+ }
+ else -> QueryType.UNKNOWN
+ }
+ }
+
+ override fun visitExpr(ctx: SQLiteParser.ExprContext): Void? {
+ val bindParameter = ctx.BIND_PARAMETER()
+ if (bindParameter != null) {
+ bindingExpressions.add(bindParameter)
+ }
+ return super.visitExpr(ctx)
+ }
+
+ fun createParsedQuery(): ParsedQuery {
+ return ParsedQuery(
+ original = original,
+ type = queryType,
+ inputs = bindingExpressions.sortedBy { it.sourceInterval.a },
+ tables = tableNames,
+ syntaxErrors = syntaxErrors,
+ runtimeQueryPlaceholder = forRuntimeQuery)
+ }
+
+ override fun visitCommon_table_expression(
+ ctx: SQLiteParser.Common_table_expressionContext): Void? {
+ val tableName = ctx.table_name()?.text
+ if (tableName != null) {
+ withClauseNames.add(unescapeIdentifier(tableName))
+ }
+ return super.visitCommon_table_expression(ctx)
+ }
+
+ override fun visitTable_or_subquery(ctx: SQLiteParser.Table_or_subqueryContext): Void? {
+ val tableName = ctx.table_name()?.text
+ if (tableName != null) {
+ val tableAlias = ctx.table_alias()?.text
+ if (tableName !in withClauseNames) {
+ tableNames.add(Table(
+ unescapeIdentifier(tableName),
+ unescapeIdentifier(tableAlias ?: tableName)))
+ }
+ }
+ return super.visitTable_or_subquery(ctx)
+ }
+
+ private fun unescapeIdentifier(text: String): String {
+ val trimmed = text.trim()
+ ESCAPE_LITERALS.forEach {
+ if (trimmed.startsWith(it) && trimmed.endsWith(it)) {
+ return unescapeIdentifier(trimmed.substring(1, trimmed.length - 1))
+ }
+ }
+ return trimmed
+ }
+
+ companion object {
+ private val ESCAPE_LITERALS = listOf("\"", "'", "`")
+ }
+}
+
+class SqlParser {
+ companion object {
+ private val INVALID_IDENTIFIER_CHARS = arrayOf('`', '\"')
+ fun parse(input: String): ParsedQuery {
+ val inputStream = ANTLRInputStream(input)
+ val lexer = SQLiteLexer(inputStream)
+ val tokenStream = CommonTokenStream(lexer)
+ val parser = SQLiteParser(tokenStream)
+ val syntaxErrors = arrayListOf<String>()
+ parser.addErrorListener(object : BaseErrorListener() {
+ override fun syntaxError(
+ recognizer: Recognizer<*, *>, offendingSymbol: Any,
+ line: Int, charPositionInLine: Int, msg: String,
+ e: RecognitionException?) {
+ syntaxErrors.add(msg)
+ }
+ })
+ try {
+ val parsed = parser.parse()
+ val statementList = parsed.sql_stmt_list()
+ if (statementList.isEmpty()) {
+ syntaxErrors.add(ParserErrors.NOT_ONE_QUERY)
+ return ParsedQuery(input, QueryType.UNKNOWN, emptyList(), emptySet(),
+ listOf(ParserErrors.NOT_ONE_QUERY), false)
+ }
+ val statements = statementList.first().children
+ .filter { it is SQLiteParser.Sql_stmtContext }
+ if (statements.size != 1) {
+ syntaxErrors.add(ParserErrors.NOT_ONE_QUERY)
+ }
+ val statement = statements.first()
+ return QueryVisitor(
+ original = input,
+ syntaxErrors = syntaxErrors,
+ statement = statement,
+ forRuntimeQuery = false).createParsedQuery()
+ } catch (antlrError: RuntimeException) {
+ return ParsedQuery(input, QueryType.UNKNOWN, emptyList(), emptySet(),
+ listOf("unknown error while parsing $input : ${antlrError.message}"),
+ false)
+ }
+ }
+
+ fun isValidIdentifier(input: String): Boolean =
+ input.isNotBlank() && INVALID_IDENTIFIER_CHARS.none { input.contains(it) }
+
+ /**
+ * creates a dummy select query for raw queries that queries the given list of tables.
+ */
+ fun rawQueryForTables(tableNames: Set<String>): ParsedQuery {
+ return ParsedQuery(
+ original = "raw query",
+ type = QueryType.UNKNOWN,
+ inputs = emptyList(),
+ tables = tableNames.map { Table(name = it, alias = it) }.toSet(),
+ syntaxErrors = emptyList(),
+ runtimeQueryPlaceholder = true
+ )
+ }
+ }
+}
+
+enum class QueryType {
+ UNKNOWN,
+ SELECT,
+ DELETE,
+ UPDATE,
+ EXPLAIN,
+ INSERT;
+
+ companion object {
+ // IF you change this, don't forget to update @Query documentation.
+ val SUPPORTED = hashSetOf(SELECT, DELETE, UPDATE)
+ }
+}
+
+enum class SQLTypeAffinity {
+ NULL,
+ TEXT,
+ INTEGER,
+ REAL,
+ BLOB;
+
+ fun getTypeMirrors(env: ProcessingEnvironment): List<TypeMirror>? {
+ val typeUtils = env.typeUtils
+ return when (this) {
+ TEXT -> listOf(env.elementUtils.getTypeElement("java.lang.String").asType())
+ INTEGER -> withBoxedTypes(env, TypeKind.INT, TypeKind.BYTE, TypeKind.CHAR,
+ TypeKind.LONG, TypeKind.SHORT)
+ REAL -> withBoxedTypes(env, TypeKind.DOUBLE, TypeKind.FLOAT)
+ BLOB -> listOf(typeUtils.getArrayType(
+ typeUtils.getPrimitiveType(TypeKind.BYTE)))
+ else -> emptyList()
+ }
+ }
+
+ private fun withBoxedTypes(env: ProcessingEnvironment, vararg primitives: TypeKind):
+ List<TypeMirror> {
+ return primitives.flatMap {
+ val primitiveType = env.typeUtils.getPrimitiveType(it)
+ listOf(primitiveType, env.typeUtils.boxedClass(primitiveType).asType())
+ }
+ }
+
+ companion object {
+ // converts from ColumnInfo#SQLiteTypeAffinity
+ fun fromAnnotationValue(value: Int): SQLTypeAffinity? {
+ return when (value) {
+ ColumnInfo.BLOB -> BLOB
+ ColumnInfo.INTEGER -> INTEGER
+ ColumnInfo.REAL -> REAL
+ ColumnInfo.TEXT -> TEXT
+ else -> null
+ }
+ }
+ }
+}
+
+enum class Collate {
+ BINARY,
+ NOCASE,
+ RTRIM,
+ LOCALIZED,
+ UNICODE;
+
+ companion object {
+ fun fromAnnotationValue(value: Int): Collate? {
+ return when (value) {
+ ColumnInfo.BINARY -> BINARY
+ ColumnInfo.NOCASE -> NOCASE
+ ColumnInfo.RTRIM -> RTRIM
+ ColumnInfo.LOCALIZED -> LOCALIZED
+ ColumnInfo.UNICODE -> UNICODE
+ else -> null
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt b/room/compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
new file mode 100644
index 0000000..2753229
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/preconditions/Checks.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.preconditions
+
+import androidx.room.ext.hasAnnotation
+import androidx.room.log.RLog
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeVariableName
+import javax.lang.model.element.Element
+import kotlin.reflect.KClass
+
+/**
+ * Similar to preconditions but element bound and just logs the error instead of throwing an
+ * exception.
+ * <p>
+ * It is important for processing to continue when some errors happen so that we can generate as
+ * much code as possible, leaving only the errors in javac output.
+ */
+class Checks(private val logger: RLog) {
+
+ fun check(predicate: Boolean, element: Element, errorMsg: String, vararg args: Any): Boolean {
+ if (!predicate) {
+ logger.e(element, errorMsg, args)
+ }
+ return predicate
+ }
+
+ fun hasAnnotation(element: Element, annotation: KClass<out Annotation>, errorMsg: String,
+ vararg args: Any): Boolean {
+ return if (!element.hasAnnotation(annotation)) {
+ logger.e(element, errorMsg, args)
+ false
+ } else {
+ true
+ }
+ }
+
+ fun notUnbound(typeName: TypeName, element: Element, errorMsg: String,
+ vararg args: Any): Boolean {
+ // TODO support bounds cases like <T extends Foo> T bar()
+ val failed = check(typeName !is TypeVariableName, element, errorMsg, args)
+ if (typeName is ParameterizedTypeName) {
+ val nestedFailure = typeName.typeArguments
+ .map { notUnbound(it, element, errorMsg, args) }
+ .any { it == true }
+ return !(failed || nestedFailure)
+ }
+ return !failed
+ }
+
+ fun notBlank(value: String?, element: Element, msg: String, vararg args: Any): Boolean {
+ return check(value != null && value.isNotBlank(), element, msg, args)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/Context.kt b/room/compiler/src/main/kotlin/androidx/room/processor/Context.kt
new file mode 100644
index 0000000..d3e61b2
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/Context.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.log.RLog
+import androidx.room.preconditions.Checks
+import androidx.room.processor.cache.Cache
+import androidx.room.solver.TypeAdapterStore
+import androidx.room.verifier.DatabaseVerifier
+import java.io.File
+import java.util.LinkedHashSet
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.type.TypeMirror
+
+class Context private constructor(
+ val processingEnv: ProcessingEnvironment,
+ val logger: RLog,
+ private val typeConverters: CustomConverterProcessor.ProcessResult,
+ private val inheritedAdapterStore: TypeAdapterStore?,
+ val cache: Cache) {
+ val checker: Checks = Checks(logger)
+ val COMMON_TYPES: Context.CommonTypes = Context.CommonTypes(processingEnv)
+
+ val typeAdapterStore by lazy {
+ if (inheritedAdapterStore != null) {
+ TypeAdapterStore.copy(this, inheritedAdapterStore)
+ } else {
+ TypeAdapterStore.create(this, typeConverters.converters)
+ }
+ }
+
+ // set when database and its entities are processed.
+ var databaseVerifier: DatabaseVerifier? = null
+
+ companion object {
+ val ARG_OPTIONS by lazy {
+ ProcessorOptions.values().map { it.argName }
+ }
+ }
+
+ constructor(processingEnv: ProcessingEnvironment) : this(
+ processingEnv = processingEnv,
+ logger = RLog(RLog.ProcessingEnvMessager(processingEnv), emptySet(), null),
+ typeConverters = CustomConverterProcessor.ProcessResult.EMPTY,
+ inheritedAdapterStore = null,
+ cache = Cache(null, LinkedHashSet(), emptySet())) {
+ }
+
+ class CommonTypes(val processingEnv: ProcessingEnvironment) {
+ val STRING: TypeMirror by lazy {
+ processingEnv.elementUtils.getTypeElement("java.lang.String").asType()
+ }
+ }
+
+ val schemaOutFolder by lazy {
+ val arg = processingEnv.options[ProcessorOptions.OPTION_SCHEMA_FOLDER.argName]
+ if (arg?.isNotEmpty() ?: false) {
+ File(arg)
+ } else {
+ null
+ }
+ }
+
+ fun <T> collectLogs(handler: (Context) -> T): Pair<T, RLog.CollectingMessager> {
+ val collector = RLog.CollectingMessager()
+ val subContext = Context(processingEnv = processingEnv,
+ logger = RLog(collector, logger.suppressedWarnings, logger.defaultElement),
+ typeConverters = this.typeConverters,
+ inheritedAdapterStore = typeAdapterStore,
+ cache = cache)
+ subContext.databaseVerifier = databaseVerifier
+ val result = handler(subContext)
+ return Pair(result, collector)
+ }
+
+ fun fork(element: Element): Context {
+ val suppressedWarnings = SuppressWarningProcessor.getSuppressedWarnings(element)
+ val processConvertersResult = CustomConverterProcessor.findConverters(this, element)
+ val canReUseAdapterStore = processConvertersResult.classes.isEmpty()
+ // order here is important since the sub context should give priority to new converters.
+ val subTypeConverters = if (canReUseAdapterStore) {
+ this.typeConverters
+ } else {
+ processConvertersResult + this.typeConverters
+ }
+ val subSuppressedWarnings = suppressedWarnings + logger.suppressedWarnings
+ val subCache = Cache(cache, subTypeConverters.classes, subSuppressedWarnings)
+ val subContext = Context(
+ processingEnv = processingEnv,
+ logger = RLog(logger.messager, subSuppressedWarnings, element),
+ typeConverters = subTypeConverters,
+ inheritedAdapterStore = if (canReUseAdapterStore) typeAdapterStore else null,
+ cache = subCache)
+ subContext.databaseVerifier = databaseVerifier
+ return subContext
+ }
+
+ enum class ProcessorOptions(val argName: String) {
+ OPTION_SCHEMA_FOLDER("room.schemaLocation")
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
new file mode 100644
index 0000000..ba8658c
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
@@ -0,0 +1,151 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.hasAnyOf
+import androidx.room.ext.toListOfClassTypes
+import androidx.room.ext.typeName
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_BAD_RETURN_TYPE
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
+import androidx.room.processor.ProcessorErrors
+ .TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_BE_PUBLIC
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_RECEIVE_1_PARAM
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_UNBOUND_GENERIC
+import androidx.room.solver.types.CustomTypeConverterWrapper
+import androidx.room.vo.CustomTypeConverter
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import java.util.LinkedHashSet
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.ElementFilter
+
+/**
+ * Processes classes that are referenced in TypeConverters annotations.
+ */
+class CustomConverterProcessor(val context: Context, val element: TypeElement) {
+ companion object {
+ private val INVALID_RETURN_TYPES = setOf(TypeKind.ERROR, TypeKind.VOID, TypeKind.NONE)
+ fun findConverters(context: Context, element: Element): ProcessResult {
+ val annotation = MoreElements.getAnnotationMirror(element,
+ TypeConverters::class.java).orNull()
+ return annotation?.let {
+ val classes = AnnotationMirrors.getAnnotationValue(annotation, "value")
+ ?.toListOfClassTypes()
+ ?.filter {
+ MoreTypes.isType(it)
+ }?.mapTo(LinkedHashSet(), { it }) ?: LinkedHashSet<TypeMirror>()
+ val converters = classes
+ .flatMap {
+ CustomConverterProcessor(context, MoreTypes.asTypeElement(it))
+ .process()
+ }
+ converters.let {
+ reportDuplicates(context, converters)
+ }
+ ProcessResult(classes, converters.map(::CustomTypeConverterWrapper))
+ } ?: ProcessResult.EMPTY
+ }
+
+ fun reportDuplicates(context: Context, converters: List<CustomTypeConverter>) {
+ val groupedByFrom = converters.groupBy { it.from.typeName() }
+ groupedByFrom.forEach {
+ it.value.groupBy { it.to.typeName() }.forEach {
+ if (it.value.size > 1) {
+ it.value.forEach { converter ->
+ context.logger.e(converter.method, ProcessorErrors
+ .duplicateTypeConverters(it.value.minus(converter)))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun process(): List<CustomTypeConverter> {
+ // using element utils instead of MoreElements to include statics.
+ val methods = ElementFilter
+ .methodsIn(context.processingEnv.elementUtils.getAllMembers(element))
+ val declaredType = MoreTypes.asDeclared(element.asType())
+ val converterMethods = methods.filter {
+ it.hasAnnotation(TypeConverter::class)
+ }
+ context.checker.check(converterMethods.isNotEmpty(), element, TYPE_CONVERTER_EMPTY_CLASS)
+ val allStatic = converterMethods.all { it.modifiers.contains(Modifier.STATIC) }
+ val constructors = ElementFilter.constructorsIn(
+ context.processingEnv.elementUtils.getAllMembers(element))
+ context.checker.check(allStatic || constructors.isEmpty() || constructors.any {
+ it.parameters.isEmpty()
+ }, element, TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
+ return converterMethods.map {
+ processMethod(declaredType, it)
+ }.filterNotNull()
+ }
+
+ private fun processMethod(
+ container: DeclaredType, methodElement: ExecutableElement): CustomTypeConverter? {
+ val asMember = context.processingEnv.typeUtils.asMemberOf(container, methodElement)
+ val executableType = MoreTypes.asExecutable(asMember)
+ val returnType = executableType.returnType
+ val invalidReturnType = INVALID_RETURN_TYPES.contains(returnType.kind)
+ context.checker.check(methodElement.hasAnyOf(Modifier.PUBLIC), methodElement,
+ TYPE_CONVERTER_MUST_BE_PUBLIC)
+ if (invalidReturnType) {
+ context.logger.e(methodElement, TYPE_CONVERTER_BAD_RETURN_TYPE)
+ return null
+ }
+ val returnTypeName = returnType.typeName()
+ context.checker.notUnbound(returnTypeName, methodElement,
+ TYPE_CONVERTER_UNBOUND_GENERIC)
+ val params = methodElement.parameters
+ if (params.size != 1) {
+ context.logger.e(methodElement, TYPE_CONVERTER_MUST_RECEIVE_1_PARAM)
+ return null
+ }
+ val param = params.map {
+ MoreTypes.asMemberOf(context.processingEnv.typeUtils, container, it)
+ }.first()
+ context.checker.notUnbound(param.typeName(), params[0], TYPE_CONVERTER_UNBOUND_GENERIC)
+ return CustomTypeConverter(container, methodElement, param, returnType)
+ }
+
+ /**
+ * Order of classes is important hence they are a LinkedHashSet not a set.
+ */
+ open class ProcessResult(
+ val classes: LinkedHashSet<TypeMirror>,
+ val converters: List<CustomTypeConverterWrapper>
+ ) {
+ object EMPTY : ProcessResult(LinkedHashSet(), emptyList())
+ operator fun plus(other: ProcessResult): ProcessResult {
+ val newClasses = LinkedHashSet<TypeMirror>()
+ newClasses.addAll(classes)
+ newClasses.addAll(other.classes)
+ return ProcessResult(newClasses, converters + other.converters)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
new file mode 100644
index 0000000..cf8ab0d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.RawQuery
+import androidx.room.SkipQueryVerification
+import androidx.room.Transaction
+import androidx.room.Update
+import androidx.room.ext.findKotlinDefaultImpl
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.hasAnyOf
+import androidx.room.ext.typeName
+import androidx.room.verifier.DatabaseVerifier
+import androidx.room.vo.Dao
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier.ABSTRACT
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+
+class DaoProcessor(baseContext: Context, val element: TypeElement, val dbType: DeclaredType,
+ val dbVerifier: DatabaseVerifier?) {
+ val context = baseContext.fork(element)
+
+ companion object {
+ val PROCESSED_ANNOTATIONS = listOf(Insert::class, Delete::class, Query::class,
+ Update::class, RawQuery::class)
+ }
+
+ fun process(): Dao {
+ context.checker.hasAnnotation(element, androidx.room.Dao::class,
+ ProcessorErrors.DAO_MUST_BE_ANNOTATED_WITH_DAO)
+ context.checker.check(element.hasAnyOf(ABSTRACT) || element.kind == ElementKind.INTERFACE,
+ element, ProcessorErrors.DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE)
+
+ val declaredType = MoreTypes.asDeclared(element.asType())
+ val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
+ val methods = allMembers
+ .filter {
+ it.hasAnyOf(ABSTRACT) && it.kind == ElementKind.METHOD
+ && it.findKotlinDefaultImpl(context.processingEnv.typeUtils) == null
+ }.map {
+ MoreElements.asExecutable(it)
+ }.groupBy { method ->
+ context.checker.check(
+ PROCESSED_ANNOTATIONS.count { method.hasAnnotation(it) } == 1, method,
+ ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD
+ )
+ if (method.hasAnnotation(Query::class)) {
+ Query::class
+ } else if (method.hasAnnotation(Insert::class)) {
+ Insert::class
+ } else if (method.hasAnnotation(Delete::class)) {
+ Delete::class
+ } else if (method.hasAnnotation(Update::class)) {
+ Update::class
+ } else if (method.hasAnnotation(RawQuery::class)) {
+ RawQuery::class
+ } else {
+ Any::class
+ }
+ }
+ val processorVerifier = if (element.hasAnnotation(SkipQueryVerification::class) ||
+ element.hasAnnotation(RawQuery::class)) {
+ null
+ } else {
+ dbVerifier
+ }
+
+ val queryMethods = methods[Query::class]?.map {
+ QueryMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = it,
+ dbVerifier = processorVerifier).process()
+ } ?: emptyList()
+
+ val rawQueryMethods = methods[RawQuery::class]?.map {
+ RawQueryMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = it
+ ).process()
+ } ?: emptyList()
+
+ val insertionMethods = methods[Insert::class]?.map {
+ InsertionMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = it).process()
+ } ?: emptyList()
+
+ val deletionMethods = methods[Delete::class]?.map {
+ DeletionMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = it).process()
+ } ?: emptyList()
+
+ val updateMethods = methods[Update::class]?.map {
+ UpdateMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = it).process()
+ } ?: emptyList()
+
+ val transactionMethods = allMembers.filter { member ->
+ member.hasAnnotation(Transaction::class)
+ && member.kind == ElementKind.METHOD
+ && PROCESSED_ANNOTATIONS.none { member.hasAnnotation(it) }
+ }.map {
+ TransactionMethodProcessor(
+ baseContext = context,
+ containing = declaredType,
+ executableElement = MoreElements.asExecutable(it)).process()
+ }
+
+ val constructors = allMembers
+ .filter { it.kind == ElementKind.CONSTRUCTOR }
+ .map { MoreElements.asExecutable(it) }
+ val typeUtils = context.processingEnv.typeUtils
+ val goodConstructor = constructors
+ .filter {
+ it.parameters.size == 1
+ && typeUtils.isAssignable(dbType, it.parameters[0].asType())
+ }
+ .firstOrNull()
+ val constructorParamType = if (goodConstructor != null) {
+ goodConstructor.parameters[0].asType().typeName()
+ } else {
+ validateEmptyConstructor(constructors)
+ null
+ }
+
+ context.checker.check(methods[Any::class] == null, element,
+ ProcessorErrors.ABSTRACT_METHOD_IN_DAO_MISSING_ANY_ANNOTATION)
+
+ val type = TypeName.get(declaredType)
+ context.checker.notUnbound(type, element,
+ ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_DAO_CLASSES)
+
+ return Dao(element = element,
+ type = declaredType,
+ queryMethods = queryMethods,
+ rawQueryMethods = rawQueryMethods,
+ insertionMethods = insertionMethods,
+ deletionMethods = deletionMethods,
+ updateMethods = updateMethods,
+ transactionMethods = transactionMethods,
+ constructorParamType = constructorParamType)
+ }
+
+ private fun validateEmptyConstructor(constructors: List<ExecutableElement>) {
+ if (constructors.isNotEmpty() && constructors.all { it.parameters.isNotEmpty() }) {
+ context.logger.e(element, ProcessorErrors.daoMustHaveMatchingConstructor(
+ element.toString(), dbType.toString()))
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
new file mode 100644
index 0000000..f4219ff
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.SkipQueryVerification
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.getAsBoolean
+import androidx.room.ext.getAsInt
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.hasAnyOf
+import androidx.room.ext.toListOfClassTypes
+import androidx.room.verifier.DatabaseVerifier
+import androidx.room.vo.Dao
+import androidx.room.vo.DaoMethod
+import androidx.room.vo.Database
+import androidx.room.vo.Entity
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeMirror
+
+class DatabaseProcessor(baseContext: Context, val element: TypeElement) {
+ val context = baseContext.fork(element)
+
+ val baseClassElement: TypeMirror by lazy {
+ context.processingEnv.elementUtils.getTypeElement(
+ RoomTypeNames.ROOM_DB.packageName() + "." + RoomTypeNames.ROOM_DB.simpleName())
+ .asType()
+ }
+
+ fun process(): Database {
+ try {
+ return doProcess()
+ } finally {
+ context.databaseVerifier?.closeConnection(context)
+ }
+ }
+
+ private fun doProcess(): Database {
+ val dbAnnotation = MoreElements
+ .getAnnotationMirror(element, androidx.room.Database::class.java)
+ .orNull()
+ val entities = processEntities(dbAnnotation, element)
+ validateUniqueTableNames(element, entities)
+ validateForeignKeys(element, entities)
+
+ val extendsRoomDb = context.processingEnv.typeUtils.isAssignable(
+ MoreElements.asType(element).asType(), baseClassElement)
+ context.checker.check(extendsRoomDb, element, ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
+
+ val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
+
+ val dbVerifier = if (element.hasAnnotation(SkipQueryVerification::class)) {
+ null
+ } else {
+ DatabaseVerifier.create(context, element, entities)
+ }
+ context.databaseVerifier = dbVerifier
+
+ val declaredType = MoreTypes.asDeclared(element.asType())
+ val daoMethods = allMembers.filter {
+ it.hasAnyOf(Modifier.ABSTRACT) && it.kind == ElementKind.METHOD
+ }.filterNot {
+ // remove methods that belong to room
+ val containing = it.enclosingElement
+ MoreElements.isType(containing) &&
+ TypeName.get(containing.asType()) == RoomTypeNames.ROOM_DB
+ }.map {
+ val executable = MoreElements.asExecutable(it)
+ // TODO when we add support for non Dao return types (e.g. database), this code needs
+ // to change
+ val daoType = MoreTypes.asTypeElement(executable.returnType)
+ val dao = DaoProcessor(context, daoType, declaredType, dbVerifier).process()
+ DaoMethod(executable, executable.simpleName.toString(), dao)
+ }
+ validateUniqueDaoClasses(element, daoMethods, entities)
+ validateUniqueIndices(element, entities)
+ val version = AnnotationMirrors.getAnnotationValue(dbAnnotation, "version")
+ .getAsInt(1)!!.toInt()
+ val exportSchema = AnnotationMirrors.getAnnotationValue(dbAnnotation, "exportSchema")
+ .getAsBoolean(true)
+
+ val hasForeignKeys = entities.any { it.foreignKeys.isNotEmpty() }
+
+ val database = Database(
+ version = version,
+ element = element,
+ type = MoreElements.asType(element).asType(),
+ entities = entities,
+ daoMethods = daoMethods,
+ exportSchema = exportSchema,
+ enableForeignKeys = hasForeignKeys)
+ return database
+ }
+
+ private fun validateForeignKeys(element: TypeElement, entities: List<Entity>) {
+ val byTableName = entities.associateBy { it.tableName }
+ entities.forEach { entity ->
+ entity.foreignKeys.forEach foreignKeyLoop@ { foreignKey ->
+ val parent = byTableName[foreignKey.parentTable]
+ if (parent == null) {
+ context.logger.e(element, ProcessorErrors
+ .foreignKeyMissingParentEntityInDatabase(foreignKey.parentTable,
+ entity.element.qualifiedName.toString()))
+ return@foreignKeyLoop
+ }
+ val parentFields = foreignKey.parentColumns.map { columnName ->
+ val parentField = parent.fields.find {
+ it.columnName == columnName
+ }
+ if (parentField == null) {
+ context.logger.e(entity.element,
+ ProcessorErrors.foreignKeyParentColumnDoesNotExist(
+ parentEntity = parent.element.qualifiedName.toString(),
+ missingColumn = columnName,
+ allColumns = parent.fields.map { it.columnName }))
+ }
+ parentField
+ }.filterNotNull()
+ if (parentFields.size != foreignKey.parentColumns.size) {
+ return@foreignKeyLoop
+ }
+ // ensure that it is indexed in the parent
+ if (!parent.isUnique(foreignKey.parentColumns)) {
+ context.logger.e(parent.element, ProcessorErrors
+ .foreignKeyMissingIndexInParent(
+ parentEntity = parent.element.qualifiedName.toString(),
+ childEntity = entity.element.qualifiedName.toString(),
+ parentColumns = foreignKey.parentColumns,
+ childColumns = foreignKey.childFields
+ .map { it.columnName }))
+ return@foreignKeyLoop
+ }
+ }
+ }
+ }
+
+ private fun validateUniqueIndices(element: TypeElement, entities: List<Entity>) {
+ entities
+ .flatMap { entity ->
+ // associate each index with its entity
+ entity.indices.map { Pair(it.name, entity) }
+ }
+ .groupBy { it.first } // group by index name
+ .filter { it.value.size > 1 } // get the ones with duplicate names
+ .forEach {
+ // do not report duplicates from the same entity
+ if (it.value.distinctBy { it.second.typeName }.size > 1) {
+ context.logger.e(element,
+ ProcessorErrors.duplicateIndexInDatabase(it.key,
+ it.value.map { "${it.second.typeName} > ${it.first}" }))
+ }
+ }
+ }
+
+ private fun validateUniqueDaoClasses(dbElement: TypeElement, daoMethods: List<DaoMethod>,
+ entities: List<Entity>) {
+ val entityTypeNames = entities.map { it.typeName }.toSet()
+ daoMethods.groupBy { it.dao.typeName }
+ .forEach {
+ if (it.value.size > 1) {
+ val error = ProcessorErrors.duplicateDao(it.key, it.value.map { it.name })
+ it.value.forEach { daoMethod ->
+ context.logger.e(daoMethod.element,
+ ProcessorErrors.DAO_METHOD_CONFLICTS_WITH_OTHERS)
+ }
+ // also report the full error for the database
+ context.logger.e(dbElement, error)
+ }
+ }
+ val check = fun(element: Element, dao: Dao,
+ typeName: TypeName?) {
+ typeName?.let {
+ if (!entityTypeNames.contains(typeName)) {
+ context.logger.e(element,
+ ProcessorErrors.shortcutEntityIsNotInDatabase(
+ database = dbElement.qualifiedName.toString(),
+ dao = dao.typeName.toString(),
+ entity = typeName.toString()
+ ))
+ }
+ }
+ }
+ daoMethods.forEach { daoMethod ->
+ daoMethod.dao.shortcutMethods.forEach { method ->
+ method.entities.forEach {
+ check(method.element, daoMethod.dao, it.value.typeName)
+ }
+ }
+ daoMethod.dao.insertionMethods.forEach { method ->
+ method.entities.forEach {
+ check(method.element, daoMethod.dao, it.value.typeName)
+ }
+ }
+ }
+ }
+
+ private fun validateUniqueTableNames(dbElement: TypeElement, entities: List<Entity>) {
+ entities
+ .groupBy {
+ it.tableName.toLowerCase()
+ }.filter {
+ it.value.size > 1
+ }.forEach { byTableName ->
+ val error = ProcessorErrors.duplicateTableNames(byTableName.key,
+ byTableName.value.map { it.typeName.toString() })
+ // report it for each of them and the database to make it easier
+ // for the developer
+ byTableName.value.forEach { entity ->
+ context.logger.e(entity.element, error)
+ }
+ context.logger.e(dbElement, error)
+ }
+ }
+
+ private fun processEntities(dbAnnotation: AnnotationMirror?, element: TypeElement):
+ List<Entity> {
+ if (!context.checker.check(dbAnnotation != null, element,
+ ProcessorErrors.DATABASE_MUST_BE_ANNOTATED_WITH_DATABASE)) {
+ return listOf()
+ }
+
+ val entityList = AnnotationMirrors.getAnnotationValue(dbAnnotation, "entities")
+ val listOfTypes = entityList.toListOfClassTypes()
+ context.checker.check(listOfTypes.isNotEmpty(), element,
+ ProcessorErrors.DATABASE_ANNOTATION_MUST_HAVE_LIST_OF_ENTITIES)
+ return listOfTypes.map {
+ EntityProcessor(context, MoreTypes.asTypeElement(it)).process()
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DeletionMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DeletionMethodProcessor.kt
new file mode 100644
index 0000000..fc375ea
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DeletionMethodProcessor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Delete
+import androidx.room.ext.typeName
+import androidx.room.vo.DeletionMethod
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+class DeletionMethodProcessor(baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+ val context = baseContext.fork(executableElement)
+
+ fun process(): DeletionMethod {
+ val delegate = ShortcutMethodProcessor(context, containing, executableElement)
+ delegate.extractAnnotation(Delete::class, ProcessorErrors.MISSING_DELETE_ANNOTATION)
+
+ val returnTypeName = delegate.extractReturnType().typeName()
+ context.checker.check(
+ returnTypeName == TypeName.VOID || returnTypeName == TypeName.INT,
+ executableElement,
+ ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
+ )
+
+ val (entities, params) = delegate.extractParams(
+ missingParamError = ProcessorErrors
+ .DELETION_MISSING_PARAMS
+ )
+
+ return DeletionMethod(
+ element = delegate.executableElement,
+ name = delegate.executableElement.simpleName.toString(),
+ entities = entities,
+ returnCount = returnTypeName == TypeName.INT,
+ parameters = params
+ )
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
new file mode 100644
index 0000000..7d8b1f5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/EntityProcessor.kt
@@ -0,0 +1,589 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.ext.getAsBoolean
+import androidx.room.ext.getAsInt
+import androidx.room.ext.getAsString
+import androidx.room.ext.getAsStringList
+import androidx.room.ext.toType
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.parser.SqlParser
+import androidx.room.processor.ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY
+import androidx.room.processor.ProcessorErrors.RELATION_IN_ENTITY
+import androidx.room.processor.cache.Cache
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Entity
+import androidx.room.vo.Field
+import androidx.room.vo.ForeignKey
+import androidx.room.vo.ForeignKeyAction
+import androidx.room.vo.Index
+import androidx.room.vo.Pojo
+import androidx.room.vo.PrimaryKey
+import androidx.room.vo.Warning
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.AnnotationMirrors.getAnnotationValue
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Name
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.SimpleAnnotationValueVisitor6
+
+class EntityProcessor(baseContext: Context,
+ val element: TypeElement,
+ private val referenceStack: LinkedHashSet<Name> = LinkedHashSet()) {
+ val context = baseContext.fork(element)
+
+ fun process(): Entity {
+ return context.cache.entities.get(Cache.EntityKey(element), {
+ doProcess()
+ })
+ }
+ private fun doProcess(): Entity {
+ context.checker.hasAnnotation(element, androidx.room.Entity::class,
+ ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY)
+ val pojo = PojoProcessor(
+ baseContext = context,
+ element = element,
+ bindingScope = FieldProcessor.BindingScope.TWO_WAY,
+ parent = null,
+ referenceStack = referenceStack).process()
+ context.checker.check(pojo.relations.isEmpty(), element, RELATION_IN_ENTITY)
+ val annotation = MoreElements.getAnnotationMirror(element,
+ androidx.room.Entity::class.java).orNull()
+ val tableName: String
+ val entityIndices: List<IndexInput>
+ val foreignKeyInputs: List<ForeignKeyInput>
+ val inheritSuperIndices: Boolean
+ if (annotation != null) {
+ tableName = extractTableName(element, annotation)
+ entityIndices = extractIndices(annotation, tableName)
+ inheritSuperIndices = AnnotationMirrors
+ .getAnnotationValue(annotation, "inheritSuperIndices").getAsBoolean(false)
+ foreignKeyInputs = extractForeignKeys(annotation)
+ } else {
+ tableName = element.simpleName.toString()
+ foreignKeyInputs = emptyList()
+ entityIndices = emptyList()
+ inheritSuperIndices = false
+ }
+ context.checker.notBlank(tableName, element,
+ ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
+
+ val fieldIndices = pojo.fields
+ .filter { it.indexed }
+ .map {
+ if (it.parent != null) {
+ it.indexed = false
+ context.logger.w(Warning.INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED, it.element,
+ ProcessorErrors.droppedEmbeddedFieldIndex(
+ it.getPath(), element.qualifiedName.toString()))
+ null
+ } else if (it.element.enclosingElement != element && !inheritSuperIndices) {
+ it.indexed = false
+ context.logger.w(Warning.INDEX_FROM_PARENT_FIELD_IS_DROPPED,
+ ProcessorErrors.droppedSuperClassFieldIndex(
+ it.columnName, element.toString(),
+ it.element.enclosingElement.toString()
+ ))
+ null
+ } else {
+ IndexInput(
+ name = createIndexName(listOf(it.columnName), tableName),
+ unique = false,
+ columnNames = listOf(it.columnName)
+ )
+ }
+ }.filterNotNull()
+ val superIndices = loadSuperIndices(element.superclass, tableName, inheritSuperIndices)
+ val indexInputs = entityIndices + fieldIndices + superIndices
+ val indices = validateAndCreateIndices(indexInputs, pojo)
+
+ val primaryKey = findAndValidatePrimaryKey(pojo.fields, pojo.embeddedFields)
+ val affinity = primaryKey.fields.firstOrNull()?.affinity ?: SQLTypeAffinity.TEXT
+ context.checker.check(
+ !primaryKey.autoGenerateId || affinity == SQLTypeAffinity.INTEGER,
+ primaryKey.fields.firstOrNull()?.element ?: element,
+ ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT
+ )
+
+ val entityForeignKeys = validateAndCreateForeignKeyReferences(foreignKeyInputs, pojo)
+ checkIndicesForForeignKeys(entityForeignKeys, primaryKey, indices)
+
+ context.checker.check(SqlParser.isValidIdentifier(tableName), element,
+ ProcessorErrors.INVALID_TABLE_NAME)
+ pojo.fields.forEach {
+ context.checker.check(SqlParser.isValidIdentifier(it.columnName), it.element,
+ ProcessorErrors.INVALID_COLUMN_NAME)
+ }
+
+ val entity = Entity(element = element,
+ tableName = tableName,
+ type = pojo.type,
+ fields = pojo.fields,
+ embeddedFields = pojo.embeddedFields,
+ indices = indices,
+ primaryKey = primaryKey,
+ foreignKeys = entityForeignKeys,
+ constructor = pojo.constructor)
+
+ return entity
+ }
+
+ private fun checkIndicesForForeignKeys(entityForeignKeys: List<ForeignKey>,
+ primaryKey: PrimaryKey,
+ indices: List<Index>) {
+ fun covers(columnNames: List<String>, fields: List<Field>): Boolean =
+ fields.size >= columnNames.size && columnNames.withIndex().all {
+ fields[it.index].columnName == it.value
+ }
+
+ entityForeignKeys.forEach { fKey ->
+ val columnNames = fKey.childFields.map { it.columnName }
+ val exists = covers(columnNames, primaryKey.fields) || indices.any { index ->
+ covers(columnNames, index.fields)
+ }
+ if (!exists) {
+ if (columnNames.size == 1) {
+ context.logger.w(Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element,
+ ProcessorErrors.foreignKeyMissingIndexInChildColumn(columnNames[0]))
+ } else {
+ context.logger.w(Warning.MISSING_INDEX_ON_FOREIGN_KEY_CHILD, element,
+ ProcessorErrors.foreignKeyMissingIndexInChildColumns(columnNames))
+ }
+ }
+ }
+ }
+
+ /**
+ * Does a validation on foreign keys except the parent table's columns.
+ */
+ private fun validateAndCreateForeignKeyReferences(foreignKeyInputs: List<ForeignKeyInput>,
+ pojo: Pojo): List<ForeignKey> {
+ return foreignKeyInputs.map {
+ if (it.onUpdate == null) {
+ context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
+ return@map null
+ }
+ if (it.onDelete == null) {
+ context.logger.e(element, ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
+ return@map null
+ }
+ if (it.childColumns.isEmpty()) {
+ context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
+ return@map null
+ }
+ if (it.parentColumns.isEmpty()) {
+ context.logger.e(element, ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
+ return@map null
+ }
+ if (it.childColumns.size != it.parentColumns.size) {
+ context.logger.e(element, ProcessorErrors.foreignKeyColumnNumberMismatch(
+ it.childColumns, it.parentColumns
+ ))
+ return@map null
+ }
+ val parentElement = try {
+ MoreTypes.asElement(it.parent) as TypeElement
+ } catch (noClass: IllegalArgumentException) {
+ context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT)
+ return@map null
+ }
+ val parentAnnotation = MoreElements.getAnnotationMirror(parentElement,
+ androidx.room.Entity::class.java).orNull()
+ if (parentAnnotation == null) {
+ context.logger.e(element,
+ ProcessorErrors.foreignKeyNotAnEntity(parentElement.toString()))
+ return@map null
+ }
+ val tableName = extractTableName(parentElement, parentAnnotation)
+ val fields = it.childColumns.map { columnName ->
+ val field = pojo.fields.find { it.columnName == columnName }
+ if (field == null) {
+ context.logger.e(pojo.element,
+ ProcessorErrors.foreignKeyChildColumnDoesNotExist(columnName,
+ pojo.fields.map { it.columnName }))
+ }
+ field
+ }.filterNotNull()
+ if (fields.size != it.childColumns.size) {
+ return@map null
+ }
+ ForeignKey(
+ parentTable = tableName,
+ childFields = fields,
+ parentColumns = it.parentColumns,
+ onDelete = it.onDelete,
+ onUpdate = it.onUpdate,
+ deferred = it.deferred
+ )
+ }.filterNotNull()
+ }
+
+ private fun findAndValidatePrimaryKey(
+ fields: List<Field>, embeddedFields: List<EmbeddedField>): PrimaryKey {
+ val candidates = collectPrimaryKeysFromEntityAnnotations(element, fields) +
+ collectPrimaryKeysFromPrimaryKeyAnnotations(fields) +
+ collectPrimaryKeysFromEmbeddedFields(embeddedFields)
+
+ context.checker.check(candidates.isNotEmpty(), element, ProcessorErrors.MISSING_PRIMARY_KEY)
+
+ // 1. If a key is not autogenerated, but is Primary key or is part of Primary key we
+ // force the @NonNull annotation. If the key is a single Primary Key, Integer or Long, we
+ // don't force the @NonNull annotation since SQLite will automatically generate IDs.
+ // 2. If a key is autogenerate, we generate NOT NULL in table spec, but we don't require
+ // @NonNull annotation on the field itself.
+ candidates.filter { candidate -> !candidate.autoGenerateId }
+ .map { candidate ->
+ candidate.fields.map { field ->
+ if (candidate.fields.size > 1 ||
+ (candidate.fields.size == 1
+ && field.affinity != SQLTypeAffinity.INTEGER)) {
+ context.checker.check(field.nonNull, field.element,
+ ProcessorErrors.primaryKeyNull(field.getPath()))
+ // Validate parents for nullability
+ var parent = field.parent
+ while (parent != null) {
+ val parentField = parent.field
+ context.checker.check(parentField.nonNull,
+ parentField.element,
+ ProcessorErrors.primaryKeyNull(parentField.getPath()))
+ parent = parentField.parent
+ }
+ }
+ }
+ }
+
+ if (candidates.size == 1) {
+ // easy :)
+ return candidates.first()
+ }
+
+ return choosePrimaryKey(candidates, element)
+ }
+
+ /**
+ * Check fields for @PrimaryKey.
+ */
+ private fun collectPrimaryKeysFromPrimaryKeyAnnotations(fields: List<Field>): List<PrimaryKey> {
+ return fields.map { field ->
+ MoreElements.getAnnotationMirror(field.element,
+ androidx.room.PrimaryKey::class.java).orNull()?.let {
+ if (field.parent != null) {
+ // the field in the entity that contains this error.
+ val grandParentField = field.parent.mRootParent.field.element
+ // bound for entity.
+ context.fork(grandParentField).logger.w(
+ Warning.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED,
+ grandParentField,
+ ProcessorErrors.embeddedPrimaryKeyIsDropped(
+ element.qualifiedName.toString(), field.name))
+ null
+ } else {
+ PrimaryKey(declaredIn = field.element.enclosingElement,
+ fields = listOf(field),
+ autoGenerateId = AnnotationMirrors
+ .getAnnotationValue(it, "autoGenerate")
+ .getAsBoolean(false))
+ }
+ }
+ }.filterNotNull()
+ }
+
+ /**
+ * Check classes for @Entity(primaryKeys = ?).
+ */
+ private fun collectPrimaryKeysFromEntityAnnotations(
+ typeElement: TypeElement, availableFields: List<Field>): List<PrimaryKey> {
+ val myPkeys = MoreElements.getAnnotationMirror(typeElement,
+ androidx.room.Entity::class.java).orNull()?.let {
+ val primaryKeyColumns = AnnotationMirrors.getAnnotationValue(it, "primaryKeys")
+ .getAsStringList()
+ if (primaryKeyColumns.isEmpty()) {
+ emptyList<PrimaryKey>()
+ } else {
+ val fields = primaryKeyColumns.map { pKeyColumnName ->
+ val field = availableFields.firstOrNull { it.columnName == pKeyColumnName }
+ context.checker.check(field != null, typeElement,
+ ProcessorErrors.primaryKeyColumnDoesNotExist(pKeyColumnName,
+ availableFields.map { it.columnName }))
+ field
+ }.filterNotNull()
+ listOf(PrimaryKey(declaredIn = typeElement,
+ fields = fields,
+ autoGenerateId = false))
+ }
+ } ?: emptyList<PrimaryKey>()
+ // checks supers.
+ val mySuper = typeElement.superclass
+ val superPKeys = if (mySuper != null && mySuper.kind != TypeKind.NONE) {
+ // my super cannot see my fields so remove them.
+ val remainingFields = availableFields.filterNot {
+ it.element.enclosingElement == typeElement
+ }
+ collectPrimaryKeysFromEntityAnnotations(
+ MoreTypes.asTypeElement(mySuper), remainingFields)
+ } else {
+ emptyList()
+ }
+ return superPKeys + myPkeys
+ }
+
+ private fun collectPrimaryKeysFromEmbeddedFields(
+ embeddedFields: List<EmbeddedField>): List<PrimaryKey> {
+ return embeddedFields.map { embeddedField ->
+ MoreElements.getAnnotationMirror(embeddedField.field.element,
+ androidx.room.PrimaryKey::class.java).orNull()?.let {
+ val autoGenerate = AnnotationMirrors
+ .getAnnotationValue(it, "autoGenerate").getAsBoolean(false)
+ context.checker.check(!autoGenerate || embeddedField.pojo.fields.size == 1,
+ embeddedField.field.element,
+ ProcessorErrors.AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS)
+ PrimaryKey(declaredIn = embeddedField.field.element.enclosingElement,
+ fields = embeddedField.pojo.fields,
+ autoGenerateId = autoGenerate)
+ }
+ }.filterNotNull()
+ }
+
+ // start from my element and check if anywhere in the list we can find the only well defined
+ // pkey, if so, use it.
+ private fun choosePrimaryKey(
+ candidates: List<PrimaryKey>, typeElement: TypeElement): PrimaryKey {
+ // If 1 of these primary keys is declared in this class, then it is the winner. Just print
+ // a note for the others.
+ // If 0 is declared, check the parent.
+ // If more than 1 primary key is declared in this class, it is an error.
+ val myPKeys = candidates.filter { candidate ->
+ candidate.declaredIn == typeElement
+ }
+ return if (myPKeys.size == 1) {
+ // just note, this is not worth an error or warning
+ (candidates - myPKeys).forEach {
+ context.logger.d(element,
+ "${it.toHumanReadableString()} is" +
+ " overridden by ${myPKeys.first().toHumanReadableString()}")
+ }
+ myPKeys.first()
+ } else if (myPKeys.isEmpty()) {
+ // i have not declared anything, delegate to super
+ val mySuper = typeElement.superclass
+ if (mySuper != null && mySuper.kind != TypeKind.NONE) {
+ return choosePrimaryKey(candidates, MoreTypes.asTypeElement(mySuper))
+ }
+ PrimaryKey.MISSING
+ } else {
+ context.logger.e(element, ProcessorErrors.multiplePrimaryKeyAnnotations(
+ myPKeys.map(PrimaryKey::toHumanReadableString)))
+ PrimaryKey.MISSING
+ }
+ }
+
+ private fun validateAndCreateIndices(
+ inputs: List<IndexInput>, pojo: Pojo): List<Index> {
+ // check for columns
+ val indices = inputs.map { input ->
+ context.checker.check(input.columnNames.isNotEmpty(), element,
+ INDEX_COLUMNS_CANNOT_BE_EMPTY)
+ val fields = input.columnNames.map { columnName ->
+ val field = pojo.fields.firstOrNull {
+ it.columnName == columnName
+ }
+ context.checker.check(field != null, element,
+ ProcessorErrors.indexColumnDoesNotExist(
+ columnName, pojo.fields.map { it.columnName }
+ ))
+ field
+ }.filterNotNull()
+ if (fields.isEmpty()) {
+ null
+ } else {
+ Index(name = input.name, unique = input.unique, fields = fields)
+ }
+ }.filterNotNull()
+
+ // check for duplicate indices
+ indices
+ .groupBy { it.name }
+ .filter { it.value.size > 1 }
+ .forEach {
+ context.logger.e(element, ProcessorErrors.duplicateIndexInEntity(it.key))
+ }
+
+ // see if any embedded field is an entity with indices, if so, report a warning
+ pojo.embeddedFields.forEach { embedded ->
+ val embeddedElement = embedded.pojo.element
+ val subEntityAnnotation = MoreElements.getAnnotationMirror(embeddedElement,
+ androidx.room.Entity::class.java).orNull()
+ subEntityAnnotation?.let {
+ val subIndices = extractIndices(subEntityAnnotation, "")
+ if (subIndices.isNotEmpty()) {
+ context.logger.w(Warning.INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED,
+ embedded.field.element, ProcessorErrors.droppedEmbeddedIndex(
+ entityName = embedded.pojo.typeName.toString(),
+ fieldPath = embedded.field.getPath(),
+ grandParent = element.qualifiedName.toString()))
+ }
+ }
+ }
+ return indices
+ }
+
+ // check if parent is an Entity, if so, report its annotation indices
+ private fun loadSuperIndices(
+ typeMirror: TypeMirror?, tableName: String, inherit: Boolean): List<IndexInput> {
+ if (typeMirror == null || typeMirror.kind == TypeKind.NONE) {
+ return emptyList()
+ }
+ val parentElement = MoreTypes.asTypeElement(typeMirror)
+ val myIndices = MoreElements.getAnnotationMirror(parentElement,
+ androidx.room.Entity::class.java).orNull()?.let { annotation ->
+ val indices = extractIndices(annotation, tableName = "super")
+ if (indices.isEmpty()) {
+ emptyList()
+ } else if (inherit) {
+ // rename them
+ indices.map {
+ IndexInput(
+ name = createIndexName(it.columnNames, tableName),
+ unique = it.unique,
+ columnNames = it.columnNames)
+ }
+ } else {
+ context.logger.w(Warning.INDEX_FROM_PARENT_IS_DROPPED,
+ parentElement,
+ ProcessorErrors.droppedSuperClassIndex(
+ childEntity = element.qualifiedName.toString(),
+ superEntity = parentElement.qualifiedName.toString()))
+ emptyList()
+ }
+ } ?: emptyList()
+ return myIndices + loadSuperIndices(parentElement.superclass, tableName, inherit)
+ }
+
+ companion object {
+ fun extractTableName(element: TypeElement, annotation: AnnotationMirror): String {
+ val annotationValue = AnnotationMirrors
+ .getAnnotationValue(annotation, "tableName").value.toString()
+ return if (annotationValue == "") {
+ element.simpleName.toString()
+ } else {
+ annotationValue
+ }
+ }
+
+ private fun extractIndices(
+ annotation: AnnotationMirror, tableName: String): List<IndexInput> {
+ val arrayOfIndexAnnotations = AnnotationMirrors.getAnnotationValue(annotation,
+ "indices")
+ return INDEX_LIST_VISITOR.visit(arrayOfIndexAnnotations, tableName)
+ }
+
+ private val INDEX_LIST_VISITOR = object
+ : SimpleAnnotationValueVisitor6<List<IndexInput>, String>() {
+ override fun visitArray(
+ values: MutableList<out AnnotationValue>?,
+ tableName: String
+ ): List<IndexInput> {
+ return values?.map {
+ INDEX_VISITOR.visit(it, tableName)
+ }?.filterNotNull() ?: emptyList<IndexInput>()
+ }
+ }
+
+ private val INDEX_VISITOR = object : SimpleAnnotationValueVisitor6<IndexInput?, String>() {
+ override fun visitAnnotation(a: AnnotationMirror?, tableName: String): IndexInput? {
+ val fieldInput = getAnnotationValue(a, "value").getAsStringList()
+ val unique = getAnnotationValue(a, "unique").getAsBoolean(false)
+ val nameValue = getAnnotationValue(a, "name")
+ .getAsString("")
+ val name = if (nameValue == null || nameValue == "") {
+ createIndexName(fieldInput, tableName)
+ } else {
+ nameValue
+ }
+ return IndexInput(name, unique, fieldInput)
+ }
+ }
+
+ private fun createIndexName(columnNames: List<String>, tableName: String): String {
+ return Index.DEFAULT_PREFIX + tableName + "_" + columnNames.joinToString("_")
+ }
+
+ private fun extractForeignKeys(annotation: AnnotationMirror): List<ForeignKeyInput> {
+ val arrayOfForeignKeyAnnotations = getAnnotationValue(annotation, "foreignKeys")
+ return FOREIGN_KEY_LIST_VISITOR.visit(arrayOfForeignKeyAnnotations)
+ }
+
+ private val FOREIGN_KEY_LIST_VISITOR = object
+ : SimpleAnnotationValueVisitor6<List<ForeignKeyInput>, Void?>() {
+ override fun visitArray(
+ values: MutableList<out AnnotationValue>?,
+ void: Void?
+ ): List<ForeignKeyInput> {
+ return values?.map {
+ FOREIGN_KEY_VISITOR.visit(it)
+ }?.filterNotNull() ?: emptyList<ForeignKeyInput>()
+ }
+ }
+
+ private val FOREIGN_KEY_VISITOR = object : SimpleAnnotationValueVisitor6<ForeignKeyInput?,
+ Void?>() {
+ override fun visitAnnotation(a: AnnotationMirror?, void: Void?): ForeignKeyInput? {
+ val entityClass = try {
+ getAnnotationValue(a, "entity").toType()
+ } catch (notPresent: TypeNotPresentException) {
+ return null
+ }
+ val parentColumns = getAnnotationValue(a, "parentColumns").getAsStringList()
+ val childColumns = getAnnotationValue(a, "childColumns").getAsStringList()
+ val onDeleteInput = getAnnotationValue(a, "onDelete").getAsInt()
+ val onUpdateInput = getAnnotationValue(a, "onUpdate").getAsInt()
+ val deferred = getAnnotationValue(a, "deferred").getAsBoolean(true)
+ val onDelete = ForeignKeyAction.fromAnnotationValue(onDeleteInput)
+ val onUpdate = ForeignKeyAction.fromAnnotationValue(onUpdateInput)
+ return ForeignKeyInput(
+ parent = entityClass,
+ parentColumns = parentColumns,
+ childColumns = childColumns,
+ onDelete = onDelete,
+ onUpdate = onUpdate,
+ deferred = deferred)
+ }
+ }
+ }
+
+ /**
+ * processed Index annotation output
+ */
+ data class IndexInput(val name: String, val unique: Boolean, val columnNames: List<String>)
+
+ /**
+ * ForeignKey, before it is processed in the context of a database.
+ */
+ data class ForeignKeyInput(
+ val parent: TypeMirror,
+ val parentColumns: List<String>,
+ val childColumns: List<String>,
+ val onDelete: ForeignKeyAction?,
+ val onUpdate: ForeignKeyAction?,
+ val deferred: Boolean)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
new file mode 100644
index 0000000..496565b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.ColumnInfo
+import androidx.room.ext.getAsBoolean
+import androidx.room.ext.getAsInt
+import androidx.room.ext.getAsString
+import androidx.room.parser.Collate
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Field
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.Element
+import javax.lang.model.type.DeclaredType
+
+class FieldProcessor(baseContext: Context, val containing: DeclaredType, val element: Element,
+ val bindingScope: BindingScope,
+ // pass only if this is processed as a child of Embedded field
+ val fieldParent: EmbeddedField?) {
+ val context = baseContext.fork(element)
+ fun process(): Field {
+ val member = context.processingEnv.typeUtils.asMemberOf(containing, element)
+ val type = TypeName.get(member)
+ val columnInfoAnnotation = MoreElements.getAnnotationMirror(element,
+ ColumnInfo::class.java)
+ val name = element.simpleName.toString()
+ val columnName: String
+ val affinity: SQLTypeAffinity?
+ val collate: Collate?
+ val fieldPrefix = fieldParent?.prefix ?: ""
+ val indexed: Boolean
+ if (columnInfoAnnotation.isPresent) {
+ val nameInAnnotation = AnnotationMirrors
+ .getAnnotationValue(columnInfoAnnotation.get(), "name")
+ .getAsString(ColumnInfo.INHERIT_FIELD_NAME)
+ columnName = fieldPrefix + if (nameInAnnotation == ColumnInfo.INHERIT_FIELD_NAME) {
+ name
+ } else {
+ nameInAnnotation
+ }
+
+ affinity = try {
+ val userDefinedAffinity = AnnotationMirrors
+ .getAnnotationValue(columnInfoAnnotation.get(), "typeAffinity")
+ .getAsInt(ColumnInfo.UNDEFINED)!!
+ SQLTypeAffinity.fromAnnotationValue(userDefinedAffinity)
+ } catch (ex: NumberFormatException) {
+ null
+ }
+
+ collate = Collate.fromAnnotationValue(AnnotationMirrors.getAnnotationValue(
+ columnInfoAnnotation.get(), "collate").getAsInt(ColumnInfo.UNSPECIFIED)!!)
+
+ indexed = AnnotationMirrors
+ .getAnnotationValue(columnInfoAnnotation.get(), "index")
+ .getAsBoolean(false)
+ } else {
+ columnName = fieldPrefix + name
+ affinity = null
+ collate = null
+ indexed = false
+ }
+ context.checker.notBlank(columnName, element,
+ ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY)
+ context.checker.notUnbound(type, element,
+ ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
+
+ val field = Field(name = name,
+ type = member,
+ element = element,
+ columnName = columnName,
+ affinity = affinity,
+ collate = collate,
+ parent = fieldParent,
+ indexed = indexed)
+
+ when (bindingScope) {
+ BindingScope.TWO_WAY -> {
+ val adapter = context.typeAdapterStore.findColumnTypeAdapter(field.type,
+ field.affinity)
+ field.statementBinder = adapter
+ field.cursorValueReader = adapter
+ field.affinity = adapter?.typeAffinity ?: field.affinity
+ context.checker.check(adapter != null, field.element,
+ ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER)
+ }
+ BindingScope.BIND_TO_STMT -> {
+ field.statementBinder = context.typeAdapterStore
+ .findStatementValueBinder(field.type, field.affinity)
+ context.checker.check(field.statementBinder != null, field.element,
+ ProcessorErrors.CANNOT_FIND_STMT_BINDER)
+ }
+ BindingScope.READ_FROM_CURSOR -> {
+ field.cursorValueReader = context.typeAdapterStore
+ .findCursorValueReader(field.type, field.affinity)
+ context.checker.check(field.cursorValueReader != null, field.element,
+ ProcessorErrors.CANNOT_FIND_CURSOR_READER)
+ }
+ }
+ return field
+ }
+
+ /**
+ * Defines what we need to assign
+ */
+ enum class BindingScope {
+ TWO_WAY, // both bind and read.
+ BIND_TO_STMT, // just value to statement
+ READ_FROM_CURSOR // just cursor to value
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/InsertionMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/InsertionMethodProcessor.kt
new file mode 100644
index 0000000..01ec6a5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/InsertionMethodProcessor.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+
+package androidx.room.processor
+
+import androidx.annotation.VisibleForTesting
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy.IGNORE
+import androidx.room.OnConflictStrategy.REPLACE
+import androidx.room.vo.InsertionMethod
+import androidx.room.vo.InsertionMethod.Type
+import androidx.room.vo.ShortcutQueryParameter
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeKind.LONG
+import javax.lang.model.type.TypeKind.VOID
+import javax.lang.model.type.TypeMirror
+
+class InsertionMethodProcessor(baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+ val context = baseContext.fork(executableElement)
+ fun process(): InsertionMethod {
+ val delegate = ShortcutMethodProcessor(context, containing, executableElement)
+ val annotation = delegate.extractAnnotation(Insert::class,
+ ProcessorErrors.MISSING_INSERT_ANNOTATION)
+
+ val onConflict = OnConflictProcessor.extractFrom(annotation)
+ context.checker.check(onConflict <= IGNORE && onConflict >= REPLACE,
+ executableElement, ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+
+ val returnType = delegate.extractReturnType()
+ val returnTypeName = TypeName.get(returnType)
+ context.checker.notUnbound(returnTypeName, executableElement,
+ ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_INSERTION_METHODS)
+
+ val (entities, params) = delegate.extractParams(
+ missingParamError = ProcessorErrors
+ .INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT
+ )
+
+ // TODO we can support more types
+ var insertionType = getInsertionType(returnType)
+ context.checker.check(insertionType != null, executableElement,
+ ProcessorErrors.INVALID_INSERTION_METHOD_RETURN_TYPE)
+
+ if (insertionType != null) {
+ val acceptable = acceptableTypes(params)
+ if (insertionType !in acceptable) {
+ context.logger.e(executableElement,
+ ProcessorErrors.insertionMethodReturnTypeMismatch(
+ insertionType.returnTypeName,
+ acceptable.map { it.returnTypeName }))
+ // clear it, no reason to generate code for it.
+ insertionType = null
+ }
+ }
+ return InsertionMethod(
+ element = executableElement,
+ name = executableElement.simpleName.toString(),
+ returnType = returnType,
+ entities = entities,
+ parameters = params,
+ onConflict = onConflict,
+ insertionType = insertionType
+ )
+ }
+
+ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ private fun getInsertionType(returnType: TypeMirror): InsertionMethod.Type? {
+ // TODO we need to support more types here.
+ fun isLongPrimitiveType(typeMirror: TypeMirror) = typeMirror.kind == LONG
+
+ fun isLongBoxType(typeMirror: TypeMirror) =
+ MoreTypes.isType(typeMirror) &&
+ MoreTypes.isTypeOf(java.lang.Long::class.java, typeMirror)
+
+ fun isLongType(typeMirror: TypeMirror) =
+ isLongPrimitiveType(typeMirror) || isLongBoxType(typeMirror)
+
+ return if (returnType.kind == VOID) {
+ Type.INSERT_VOID
+ } else if (returnType.kind == TypeKind.ARRAY) {
+ val arrayType = MoreTypes.asArray(returnType)
+ val param = arrayType.componentType
+ if (isLongPrimitiveType(param)) {
+ Type.INSERT_ID_ARRAY
+ } else if (isLongBoxType(param)) {
+ Type.INSERT_ID_ARRAY_BOX
+ } else {
+ null
+ }
+ } else if (MoreTypes.isType(returnType)
+ && MoreTypes.isTypeOf(List::class.java, returnType)) {
+ val declared = MoreTypes.asDeclared(returnType)
+ val param = declared.typeArguments.first()
+ if (isLongBoxType(param)) {
+ Type.INSERT_ID_LIST
+ } else {
+ null
+ }
+ } else if (isLongType(returnType)) {
+ Type.INSERT_SINGLE_ID
+ } else {
+ null
+ }
+ }
+
+ companion object {
+ @VisibleForTesting
+ val VOID_SET by lazy { setOf(Type.INSERT_VOID) }
+ @VisibleForTesting
+ val SINGLE_ITEM_SET by lazy { setOf(Type.INSERT_VOID, Type.INSERT_SINGLE_ID) }
+ @VisibleForTesting
+ val MULTIPLE_ITEM_SET by lazy {
+ setOf(Type.INSERT_VOID, Type.INSERT_ID_ARRAY, Type.INSERT_ID_ARRAY_BOX,
+ Type.INSERT_ID_LIST)
+ }
+ fun acceptableTypes(params: List<ShortcutQueryParameter>): Set<InsertionMethod.Type> {
+ if (params.isEmpty()) {
+ return VOID_SET
+ }
+ if (params.size > 1) {
+ return VOID_SET
+ }
+ if (params.first().isMultiple) {
+ return MULTIPLE_ITEM_SET
+ } else {
+ return SINGLE_ITEM_SET
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/OnConflictProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/OnConflictProcessor.kt
new file mode 100644
index 0000000..537f685
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/OnConflictProcessor.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.OnConflictStrategy
+import com.google.auto.common.AnnotationMirrors
+import javax.lang.model.element.AnnotationMirror
+
+/**
+ * Processes on conflict fields in annotations
+ */
+object OnConflictProcessor {
+ val INVALID_ON_CONFLICT = -1
+
+ @OnConflictStrategy
+ fun extractFrom(annotation: AnnotationMirror?, fieldName: String = "onConflict"): Int {
+ return if (annotation == null) {
+ INVALID_ON_CONFLICT
+ } else {
+ try {
+ val onConflictValue = AnnotationMirrors
+ .getAnnotationValue(annotation, fieldName)
+ .value
+ onConflictValue.toString().toInt()
+ } catch (ex: NumberFormatException) {
+ INVALID_ON_CONFLICT
+ }
+ }
+ }
+
+ fun onConflictText(@OnConflictStrategy onConflict: Int): String {
+ return when (onConflict) {
+ OnConflictStrategy.REPLACE -> "REPLACE"
+ OnConflictStrategy.ABORT -> "ABORT"
+ OnConflictStrategy.FAIL -> "FAIL"
+ OnConflictStrategy.IGNORE -> "IGNORE"
+ OnConflictStrategy.ROLLBACK -> "ROLLBACK"
+ else -> "BAD_CONFLICT_CONSTRAINT"
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoMethodProcessor.kt
new file mode 100644
index 0000000..fc2f33a
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoMethodProcessor.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 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 androidx.room.processor
+
+import androidx.room.vo.PojoMethod
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+/**
+ * processes an executable element as member of the owning class
+ */
+class PojoMethodProcessor(
+ private val context: Context,
+ private val element: ExecutableElement,
+ private val owner: DeclaredType) {
+ fun process(): PojoMethod {
+ val asMember = context.processingEnv.typeUtils.asMemberOf(owner, element)
+ val name = element.simpleName.toString()
+ return PojoMethod(
+ element = element,
+ resolvedType = MoreTypes.asExecutable(asMember),
+ name = name
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
new file mode 100644
index 0000000..e04cbb5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -0,0 +1,726 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.ColumnInfo
+import androidx.room.Embedded
+import androidx.room.Ignore
+import androidx.room.Relation
+import androidx.room.ext.KotlinMetadataProcessor
+import androidx.room.ext.getAllFieldsIncludingPrivateSupers
+import androidx.room.ext.getAnnotationValue
+import androidx.room.ext.getAsString
+import androidx.room.ext.getAsStringList
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.hasAnyOf
+import androidx.room.ext.isAssignableWithoutVariance
+import androidx.room.ext.isCollection
+import androidx.room.ext.toClassType
+import androidx.room.ext.typeName
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_TYPE
+import androidx.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
+import androidx.room.processor.cache.Cache
+import androidx.room.vo.CallType
+import androidx.room.vo.Constructor
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Entity
+import androidx.room.vo.Field
+import androidx.room.vo.FieldGetter
+import androidx.room.vo.FieldSetter
+import androidx.room.vo.Pojo
+import androidx.room.vo.PojoMethod
+import androidx.room.vo.Warning
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
+import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier.ABSTRACT
+import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.element.Modifier.PROTECTED
+import javax.lang.model.element.Modifier.PUBLIC
+import javax.lang.model.element.Modifier.STATIC
+import javax.lang.model.element.Modifier.TRANSIENT
+import javax.lang.model.element.Name
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.ElementFilter
+
+/**
+ * Processes any class as if it is a Pojo.
+ */
+class PojoProcessor(
+ baseContext: Context,
+ val element: TypeElement,
+ val bindingScope: FieldProcessor.BindingScope,
+ val parent: EmbeddedField?,
+ val referenceStack: LinkedHashSet<Name> = LinkedHashSet())
+ : KotlinMetadataProcessor {
+ val context = baseContext.fork(element)
+
+ // for KotlinMetadataUtils
+ override val processingEnv: ProcessingEnvironment
+ get() = context.processingEnv
+
+ // opportunistic kotlin metadata
+ private val kotlinMetadata by lazy {
+ try {
+ element.kotlinMetadata
+ } catch (throwable: Throwable) {
+ context.logger.d(element, "failed to read get kotlin metadata from %s", element)
+ } as? KotlinClassMetadata
+ }
+
+ companion object {
+ val PROCESSED_ANNOTATIONS = listOf(ColumnInfo::class, Embedded::class,
+ Relation::class)
+ }
+
+ fun process(): Pojo {
+ return context.cache.pojos.get(Cache.PojoKey(element, bindingScope, parent), {
+ referenceStack.add(element.qualifiedName)
+ try {
+ doProcess()
+ } finally {
+ referenceStack.remove(element.qualifiedName)
+ }
+ })
+ }
+
+ private fun doProcess(): Pojo {
+ val declaredType = MoreTypes.asDeclared(element.asType())
+ // TODO handle conflicts with super: b/35568142
+ val allFields = element.getAllFieldsIncludingPrivateSupers(context.processingEnv)
+ .filter {
+ !it.hasAnnotation(Ignore::class)
+ && !it.hasAnyOf(STATIC)
+ && (!it.hasAnyOf(TRANSIENT)
+ || it.hasAnnotation(ColumnInfo::class)
+ || it.hasAnnotation(Embedded::class)
+ || it.hasAnnotation(Relation::class))
+ }
+ .groupBy { field ->
+ context.checker.check(
+ PROCESSED_ANNOTATIONS.count { field.hasAnnotation(it) } < 2, field,
+ ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION
+ )
+ if (field.hasAnnotation(Embedded::class)) {
+ Embedded::class
+ } else if (field.hasAnnotation(Relation::class)) {
+ Relation::class
+ } else {
+ null
+ }
+ }
+
+ val myFields = allFields[null]
+ ?.map {
+ FieldProcessor(
+ baseContext = context,
+ containing = declaredType,
+ element = it,
+ bindingScope = bindingScope,
+ fieldParent = parent).process()
+ } ?: emptyList()
+
+ val embeddedFields =
+ allFields[Embedded::class]
+ ?.map {
+ processEmbeddedField(declaredType, it)
+ }
+ ?.filterNotNull()
+ ?: emptyList()
+
+ val subFields = embeddedFields.flatMap { it.pojo.fields }
+ val fields = myFields + subFields
+
+ val myRelationsList = allFields[Relation::class]
+ ?.map {
+ processRelationField(fields, declaredType, it)
+ }
+ ?.filterNotNull()
+ ?: emptyList()
+
+ val subRelations = embeddedFields.flatMap { it.pojo.relations }
+ val relations = myRelationsList + subRelations
+
+ fields.groupBy { it.columnName }
+ .filter { it.value.size > 1 }
+ .forEach {
+ context.logger.e(element, ProcessorErrors.pojoDuplicateFieldNames(
+ it.key, it.value.map(Field::getPath)
+ ))
+ it.value.forEach {
+ context.logger.e(it.element, POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME)
+ }
+ }
+
+ val methods = MoreElements.getLocalAndInheritedMethods(element,
+ context.processingEnv.elementUtils)
+ .filter {
+ !it.hasAnyOf(PRIVATE, ABSTRACT, STATIC)
+ && !it.hasAnnotation(Ignore::class)
+ }
+ .map { MoreElements.asExecutable(it) }
+ .map {
+ PojoMethodProcessor(
+ context = context,
+ element = it,
+ owner = declaredType
+ ).process()
+ }
+
+ val getterCandidates = methods.filter {
+ it.element.parameters.size == 0 && it.resolvedType.returnType.kind != TypeKind.VOID
+ }
+
+ val setterCandidates = methods.filter {
+ it.element.parameters.size == 1 && it.resolvedType.returnType.kind == TypeKind.VOID
+ }
+
+ // don't try to find a constructor for binding to statement.
+ val constructor = if (bindingScope == FieldProcessor.BindingScope.BIND_TO_STMT) {
+ // we don't need to construct this POJO.
+ null
+ } else {
+ chooseConstructor(myFields, embeddedFields, relations)
+ }
+
+ assignGetters(myFields, getterCandidates)
+ assignSetters(myFields, setterCandidates, constructor)
+
+ embeddedFields.forEach {
+ assignGetter(it.field, getterCandidates)
+ assignSetter(it.field, setterCandidates, constructor)
+ }
+
+ myRelationsList.forEach {
+ assignGetter(it.field, getterCandidates)
+ assignSetter(it.field, setterCandidates, constructor)
+ }
+
+ return Pojo(element = element,
+ type = declaredType,
+ fields = fields,
+ embeddedFields = embeddedFields,
+ relations = relations,
+ constructor = constructor)
+ }
+
+ /**
+ * Retrieves the parameter names of a method. If the method is inherited from a dependency
+ * module, the parameter name is not available (not in java spec). For kotlin, since parameter
+ * names are part of the API, we can read them via the kotlin metadata annotation.
+ * <p>
+ * Since we are using an unofficial library to read the metadata, all access to that code
+ * is safe guarded to avoid unexpected failures. In other words, it is a best effort but
+ * better than not supporting these until JB provides a proper API.
+ */
+ private fun getParamNames(method: ExecutableElement): List<String> {
+ val paramNames = method.parameters.map { it.simpleName.toString() }
+ if (paramNames.isEmpty()) {
+ return emptyList()
+ }
+ return kotlinMetadata?.getParameterNames(method) ?: paramNames
+ }
+
+ private fun chooseConstructor(
+ myFields: List<Field>,
+ embedded: List<EmbeddedField>,
+ relations: List<androidx.room.vo.Relation>): Constructor? {
+ val constructors = ElementFilter.constructorsIn(element.enclosedElements)
+ .filterNot { it.hasAnnotation(Ignore::class) || it.hasAnyOf(PRIVATE) }
+ val fieldMap = myFields.associateBy { it.name }
+ val embeddedMap = embedded.associateBy { it.field.name }
+ val typeUtils = context.processingEnv.typeUtils
+ // list of param names -> matched params pairs for each failed constructor
+ val failedConstructors = arrayListOf<FailedConstructor>()
+ // if developer puts a relation into a constructor, it is usually an error but if there
+ // is another constructor that is good, we can ignore the error. b/72884434
+ val relationsInConstructor = arrayListOf<VariableElement>()
+ val goodConstructors = constructors.map { constructor ->
+ val parameterNames = getParamNames(constructor)
+ val params = constructor.parameters.mapIndexed param@ { index, param ->
+ val paramName = parameterNames[index]
+ val paramType = param.asType()
+
+ val matches = fun(field: Field?): Boolean {
+ return if (field == null) {
+ false
+ } else if (!field.nameWithVariations.contains(paramName)) {
+ false
+ } else {
+ // see: b/69164099
+ typeUtils.isAssignableWithoutVariance(paramType, field.type)
+ }
+ }
+
+ val exactFieldMatch = fieldMap[paramName]
+
+ if (matches(exactFieldMatch)) {
+ return@param Constructor.FieldParam(exactFieldMatch!!)
+ }
+ val exactEmbeddedMatch = embeddedMap[paramName]
+ if (matches(exactEmbeddedMatch?.field)) {
+ return@param Constructor.EmbeddedParam(exactEmbeddedMatch!!)
+ }
+
+ val matchingFields = myFields.filter {
+ matches(it)
+ }
+ val embeddedMatches = embedded.filter {
+ matches(it.field)
+ }
+ if (matchingFields.isEmpty() && embeddedMatches.isEmpty()) {
+ // if it didn't match a proper field, a common mistake is to have a relation
+ // so check to see if it is a relation
+ val matchedRelation = relations.any {
+ it.field.nameWithVariations.contains(paramName)
+ }
+ if (matchedRelation) {
+ relationsInConstructor.add(param)
+ }
+ null
+ } else if (matchingFields.size + embeddedMatches.size == 1) {
+ if (matchingFields.isNotEmpty()) {
+ Constructor.FieldParam(matchingFields.first())
+ } else {
+ Constructor.EmbeddedParam(embeddedMatches.first())
+ }
+ } else {
+ context.logger.e(param, ProcessorErrors.ambigiousConstructor(
+ pojo = element.qualifiedName.toString(),
+ paramName = paramName,
+ matchingFields = matchingFields.map { it.getPath() }
+ + embedded.map { it.field.getPath() }
+ ))
+ null
+ }
+ }
+ if (params.any { it == null }) {
+ failedConstructors.add(FailedConstructor(constructor, parameterNames, params))
+ null
+ } else {
+ @Suppress("UNCHECKED_CAST")
+ Constructor(constructor, params as List<Constructor.Param>)
+ }
+ }.filterNotNull()
+ when {
+ goodConstructors.isEmpty() -> {
+ relationsInConstructor.forEach {
+ context.logger.e(it,
+ ProcessorErrors.RELATION_CANNOT_BE_CONSTRUCTOR_PARAMETER)
+ }
+ if (failedConstructors.isNotEmpty()) {
+ val failureMsg = failedConstructors.joinToString("\n") { entry ->
+ entry.log()
+ }
+ context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR +
+ "\nTried the following constructors but they failed to match:" +
+ "\n$failureMsg")
+ }
+ context.logger.e(element, ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ return null
+ }
+ goodConstructors.size > 1 -> {
+ // if there is a no-arg constructor, pick it. Even though it is weird, easily happens
+ // with kotlin data classes.
+ val noArg = goodConstructors.firstOrNull { it.params.isEmpty() }
+ if (noArg != null) {
+ context.logger.w(Warning.DEFAULT_CONSTRUCTOR, element,
+ ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS_CHOOSING_NO_ARG)
+ return noArg
+ }
+ goodConstructors.forEach {
+ context.logger.e(it.element, ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS)
+ }
+ return null
+ }
+ else -> return goodConstructors.first()
+ }
+ }
+
+ private fun processEmbeddedField(
+ declaredType: DeclaredType?, variableElement: VariableElement): EmbeddedField? {
+
+ val asTypeElement = MoreTypes.asTypeElement(variableElement.asType())
+
+ if (detectReferenceRecursion(asTypeElement)) {
+ return null
+ }
+
+ val fieldPrefix = variableElement
+ .getAnnotationValue(Embedded::class.java, "prefix")
+ ?.toString()
+ ?: ""
+ val inheritedPrefix = parent?.prefix ?: ""
+ val embeddedField = Field(
+ variableElement,
+ variableElement.simpleName.toString(),
+ type = context
+ .processingEnv
+ .typeUtils
+ .asMemberOf(declaredType, variableElement),
+ affinity = null,
+ parent = parent)
+ val subParent = EmbeddedField(
+ field = embeddedField,
+ prefix = inheritedPrefix + fieldPrefix,
+ parent = parent)
+ subParent.pojo = PojoProcessor(
+ baseContext = context.fork(variableElement),
+ element = asTypeElement,
+ bindingScope = bindingScope,
+ parent = subParent,
+ referenceStack = referenceStack).process()
+ return subParent
+ }
+
+ private fun processRelationField(
+ myFields: List<Field>, container: DeclaredType?,
+ relationElement: VariableElement
+ ): androidx.room.vo.Relation? {
+ val asTypeElement = MoreTypes.asTypeElement(
+ MoreElements.asVariable(relationElement).asType())
+
+ if (detectReferenceRecursion(asTypeElement)) {
+ return null
+ }
+
+ val annotation = MoreElements.getAnnotationMirror(relationElement, Relation::class.java)
+ .orNull()!!
+ val parentColumnInput = AnnotationMirrors.getAnnotationValue(annotation, "parentColumn")
+ .getAsString("") ?: ""
+
+ val parentField = myFields.firstOrNull {
+ it.columnName == parentColumnInput
+ }
+ if (parentField == null) {
+ context.logger.e(relationElement,
+ ProcessorErrors.relationCannotFindParentEntityField(
+ entityName = element.qualifiedName.toString(),
+ columnName = parentColumnInput,
+ availableColumns = myFields.map { it.columnName }))
+ return null
+ }
+ // parse it as an entity.
+ val asMember = MoreTypes
+ .asMemberOf(context.processingEnv.typeUtils, container, relationElement)
+ if (asMember.kind == TypeKind.ERROR) {
+ context.logger.e(ProcessorErrors.CANNOT_FIND_TYPE, element)
+ return null
+ }
+ val declared = MoreTypes.asDeclared(asMember)
+ if (!declared.isCollection()) {
+ context.logger.e(relationElement, ProcessorErrors.RELATION_NOT_COLLECTION)
+ return null
+ }
+ val typeArg = declared.typeArguments.first()
+ if (typeArg.kind == TypeKind.ERROR) {
+ context.logger.e(MoreTypes.asTypeElement(typeArg), CANNOT_FIND_TYPE)
+ return null
+ }
+ val typeArgElement = MoreTypes.asTypeElement(typeArg)
+ val entityClassInput = AnnotationMirrors
+ .getAnnotationValue(annotation, "entity").toClassType()
+
+ // do we need to decide on the entity?
+ val inferEntity = (entityClassInput == null
+ || MoreTypes.isTypeOf(Any::class.java, entityClassInput))
+
+ val entity = if (inferEntity) {
+ EntityProcessor(context, typeArgElement, referenceStack).process()
+ } else {
+ EntityProcessor(context, MoreTypes.asTypeElement(entityClassInput),
+ referenceStack).process()
+ }
+
+ // now find the field in the entity.
+ val entityColumnInput = AnnotationMirrors.getAnnotationValue(annotation, "entityColumn")
+ .getAsString() ?: ""
+ val entityField = entity.fields.firstOrNull {
+ it.columnName == entityColumnInput
+ }
+
+ if (entityField == null) {
+ context.logger.e(relationElement,
+ ProcessorErrors.relationCannotFindEntityField(
+ entityName = entity.typeName.toString(),
+ columnName = entityColumnInput,
+ availableColumns = entity.fields.map { it.columnName }))
+ return null
+ }
+
+ val field = Field(
+ element = relationElement,
+ name = relationElement.simpleName.toString(),
+ type = context.processingEnv.typeUtils.asMemberOf(container, relationElement),
+ affinity = null,
+ parent = parent)
+
+ val projectionInput = AnnotationMirrors.getAnnotationValue(annotation, "projection")
+ .getAsStringList()
+ val projection = if (projectionInput.isEmpty()) {
+ // we need to infer the projection from inputs.
+ createRelationshipProjection(inferEntity, typeArg, entity, entityField, typeArgElement)
+ } else {
+ // make sure projection makes sense
+ validateRelationshipProjection(projectionInput, entity, relationElement)
+ projectionInput
+ }
+ // if types don't match, row adapter prints a warning
+ return androidx.room.vo.Relation(
+ entity = entity,
+ pojoType = typeArg,
+ field = field,
+ parentField = parentField,
+ entityField = entityField,
+ projection = projection
+ )
+ }
+
+ private fun validateRelationshipProjection(
+ projectionInput: List<String>,
+ entity: Entity,
+ relationElement: VariableElement) {
+ val missingColumns = projectionInput.filterNot { columnName ->
+ entity.fields.any { columnName == it.columnName }
+ }
+ if (missingColumns.isNotEmpty()) {
+ context.logger.e(relationElement,
+ ProcessorErrors.relationBadProject(entity.typeName.toString(),
+ missingColumns, entity.fields.map { it.columnName }))
+ }
+ }
+
+ /**
+ * Create the projection column list based on the relationship args.
+ *
+ * if entity field in the annotation is not specified, it is the method return type
+ * if it is specified in the annotation:
+ * still check the method return type, if the same, use it
+ * if not, check to see if we can find a column Adapter, if so use the childField
+ * last resort, try to parse it as a pojo to infer it.
+ */
+ private fun createRelationshipProjection(
+ inferEntity: Boolean,
+ typeArg: TypeMirror,
+ entity: Entity,
+ entityField: Field,
+ typeArgElement: TypeElement): List<String> {
+ return if (inferEntity || typeArg.typeName() == entity.typeName) {
+ entity.fields.map { it.columnName }
+ } else {
+ val columnAdapter = context.typeAdapterStore.findCursorValueReader(typeArg, null)
+ if (columnAdapter != null) {
+ // nice, there is a column adapter for this, assume single column response
+ listOf(entityField.name)
+ } else {
+ // last resort, it needs to be a pojo
+ val pojo = PojoProcessor(
+ baseContext = context,
+ element = typeArgElement,
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = parent,
+ referenceStack = referenceStack).process()
+ pojo.fields.map { it.columnName }
+ }
+ }
+ }
+
+ private fun detectReferenceRecursion(typeElement: TypeElement): Boolean {
+ if (referenceStack.contains(typeElement.qualifiedName)) {
+ context.logger.e(
+ typeElement,
+ ProcessorErrors
+ .RECURSIVE_REFERENCE_DETECTED
+ .format(computeReferenceRecursionString(typeElement)))
+ return true
+ }
+ return false
+ }
+
+ private fun computeReferenceRecursionString(typeElement: TypeElement): String {
+ val recursiveTailTypeName = typeElement.qualifiedName
+
+ val referenceRecursionList = mutableListOf<Name>()
+ with(referenceRecursionList) {
+ add(recursiveTailTypeName)
+ addAll(referenceStack.toList().takeLastWhile { it != recursiveTailTypeName })
+ add(recursiveTailTypeName)
+ }
+
+ return referenceRecursionList.joinToString(" -> ")
+ }
+
+ private fun assignGetters(fields: List<Field>, getterCandidates: List<PojoMethod>) {
+ fields.forEach { field ->
+ assignGetter(field, getterCandidates)
+ }
+ }
+
+ private fun assignGetter(field: Field, getterCandidates: List<PojoMethod>) {
+ val success = chooseAssignment(field = field,
+ candidates = getterCandidates,
+ nameVariations = field.getterNameWithVariations,
+ getType = { method ->
+ method.resolvedType.returnType
+ },
+ assignFromField = {
+ field.getter = FieldGetter(
+ name = field.name,
+ type = field.type,
+ callType = CallType.FIELD)
+ },
+ assignFromMethod = { match ->
+ field.getter = FieldGetter(
+ name = match.name,
+ type = match.resolvedType.returnType,
+ callType = CallType.METHOD)
+ },
+ reportAmbiguity = { matching ->
+ context.logger.e(field.element,
+ ProcessorErrors.tooManyMatchingGetters(field, matching))
+ })
+ context.checker.check(success, field.element, CANNOT_FIND_GETTER_FOR_FIELD)
+ }
+
+ private fun assignSetters(
+ fields: List<Field>,
+ setterCandidates: List<PojoMethod>,
+ constructor: Constructor?) {
+ fields.forEach { field ->
+ assignSetter(field, setterCandidates, constructor)
+ }
+ }
+
+ private fun assignSetter(
+ field: Field,
+ setterCandidates: List<PojoMethod>,
+ constructor: Constructor?) {
+ if (constructor != null && constructor.hasField(field)) {
+ field.setter = FieldSetter(field.name, field.type, CallType.CONSTRUCTOR)
+ return
+ }
+ val success = chooseAssignment(field = field,
+ candidates = setterCandidates,
+ nameVariations = field.setterNameWithVariations,
+ getType = { method ->
+ method.resolvedType.parameterTypes.first()
+ },
+ assignFromField = {
+ field.setter = FieldSetter(
+ name = field.name,
+ type = field.type,
+ callType = CallType.FIELD)
+ },
+ assignFromMethod = { match ->
+ val paramType = match.resolvedType.parameterTypes.first()
+ field.setter = FieldSetter(
+ name = match.name,
+ type = paramType,
+ callType = CallType.METHOD)
+ },
+ reportAmbiguity = { matching ->
+ context.logger.e(field.element,
+ ProcessorErrors.tooManyMatchingSetter(field, matching))
+ })
+ context.checker.check(success, field.element, CANNOT_FIND_SETTER_FOR_FIELD)
+ }
+
+ /**
+ * Finds a setter/getter from available list of methods.
+ * It returns true if assignment is successful, false otherwise.
+ * At worst case, it sets to the field as if it is accessible so that the rest of the
+ * compilation can continue.
+ */
+ private fun chooseAssignment(
+ field: Field,
+ candidates: List<PojoMethod>,
+ nameVariations: List<String>,
+ getType: (PojoMethod) -> TypeMirror,
+ assignFromField: () -> Unit,
+ assignFromMethod: (PojoMethod) -> Unit,
+ reportAmbiguity: (List<String>) -> Unit
+ ): Boolean {
+ if (field.element.hasAnyOf(PUBLIC)) {
+ assignFromField()
+ return true
+ }
+ val types = context.processingEnv.typeUtils
+
+ val matching = candidates
+ .filter {
+ // b/69164099
+ types.isAssignableWithoutVariance(getType(it), field.type)
+ && (field.nameWithVariations.contains(it.name)
+ || nameVariations.contains(it.name))
+ }
+ .groupBy {
+ if (it.element.hasAnyOf(PUBLIC)) PUBLIC else PROTECTED
+ }
+ if (matching.isEmpty()) {
+ // we always assign to avoid NPEs in the rest of the compilation.
+ assignFromField()
+ // if field is not private, assume it works (if we are on the same package).
+ // if not, compiler will tell, we didn't have any better alternative anyways.
+ return !field.element.hasAnyOf(PRIVATE)
+ }
+ val match = verifyAndChooseOneFrom(matching[PUBLIC], reportAmbiguity)
+ ?: verifyAndChooseOneFrom(matching[PROTECTED], reportAmbiguity)
+ if (match == null) {
+ assignFromField()
+ return false
+ } else {
+ assignFromMethod(match)
+ return true
+ }
+ }
+
+ private fun verifyAndChooseOneFrom(
+ candidates: List<PojoMethod>?,
+ reportAmbiguity: (List<String>) -> Unit
+ ): PojoMethod? {
+ if (candidates == null) {
+ return null
+ }
+ if (candidates.size > 1) {
+ reportAmbiguity(candidates.map { it.name })
+ }
+ return candidates.first()
+ }
+
+ private data class FailedConstructor(
+ val method: ExecutableElement,
+ val params: List<String>,
+ val matches: List<Constructor.Param?>
+ ) {
+ fun log(): String {
+ val logPerParam = params.withIndex().joinToString(", ") {
+ "param:${it.value} -> matched field:" + (matches[it.index]?.log() ?: "unmatched")
+ }
+ return "$method -> [$logPerParam]"
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
new file mode 100644
index 0000000..5d9e6b6
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.RawQuery
+import androidx.room.Update
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.vo.CustomTypeConverter
+import androidx.room.vo.Field
+import com.squareup.javapoet.TypeName
+
+object ProcessorErrors {
+ private fun String.trim(): String {
+ return this.trimIndent().replace("\n", " ")
+ }
+ val MISSING_QUERY_ANNOTATION = "Query methods must be annotated with ${Query::class.java}"
+ val MISSING_INSERT_ANNOTATION = "Insertion methods must be annotated with ${Insert::class.java}"
+ val MISSING_DELETE_ANNOTATION = "Deletion methods must be annotated with ${Delete::class.java}"
+ val MISSING_UPDATE_ANNOTATION = "Update methods must be annotated with ${Update::class.java}"
+ val MISSING_RAWQUERY_ANNOTATION = "RawQuery methods must be annotated with" +
+ " ${RawQuery::class.java}"
+ val INVALID_ON_CONFLICT_VALUE = "On conflict value must be one of @OnConflictStrategy values."
+ val INVALID_INSERTION_METHOD_RETURN_TYPE = "Methods annotated with @Insert can return either" +
+ " void, long, Long, long[], Long[] or List<Long>."
+ val TRANSACTION_REFERENCE_DOCS = "https://developer.android.com/reference/android/arch/" +
+ "persistence/room/Transaction.html"
+
+ fun insertionMethodReturnTypeMismatch(definedReturn: TypeName,
+ expectedReturnTypes: List<TypeName>): String {
+ return "Method returns $definedReturn but it should return one of the following: `" +
+ expectedReturnTypes.joinToString(", ") + "`. If you want to return the list of" +
+ " row ids from the query, your insertion method can receive only 1 parameter."
+ }
+
+ val ABSTRACT_METHOD_IN_DAO_MISSING_ANY_ANNOTATION = "Abstract method in DAO must be annotated" +
+ " with ${Query::class.java} AND ${Insert::class.java}"
+ val INVALID_ANNOTATION_COUNT_IN_DAO_METHOD = "An abstract DAO method must be" +
+ " annotated with one and only one of the following annotations: " +
+ DaoProcessor.PROCESSED_ANNOTATIONS.joinToString(",") {
+ it.java.simpleName
+ }
+ val CANNOT_RESOLVE_RETURN_TYPE = "Cannot resolve return type for %s"
+ val CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS = "Cannot use unbound generics in query" +
+ " methods. It must be bound to a type through base Dao class."
+ val CANNOT_USE_UNBOUND_GENERICS_IN_INSERTION_METHODS = "Cannot use unbound generics in" +
+ " insertion methods. It must be bound to a type through base Dao class."
+ val CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS = "Cannot use unbound fields in entities."
+ val CANNOT_USE_UNBOUND_GENERICS_IN_DAO_CLASSES = "Cannot use unbound generics in Dao classes." +
+ " If you are trying to create a base DAO, create a normal class, extend it with type" +
+ " params then mark the subclass with @Dao."
+ val CANNOT_FIND_GETTER_FOR_FIELD = "Cannot find getter for field."
+ val CANNOT_FIND_SETTER_FOR_FIELD = "Cannot find setter for field."
+ val MISSING_PRIMARY_KEY = "An entity must have at least 1 field annotated with @PrimaryKey"
+ val AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT = "If a primary key is annotated with" +
+ " autoGenerate, its type must be int, Integer, long or Long."
+ val AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS = "When @PrimaryKey annotation is used on a" +
+ " field annotated with @Embedded, the embedded class should have only 1 field."
+
+ fun multiplePrimaryKeyAnnotations(primaryKeys: List<String>): String {
+ return """
+ You cannot have multiple primary keys defined in an Entity. If you
+ want to declare a composite primary key, you should use @Entity#primaryKeys and
+ not use @PrimaryKey. Defined Primary Keys:
+ ${primaryKeys.joinToString(", ")}""".trim()
+ }
+
+ fun primaryKeyColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
+ return "$columnName referenced in the primary key does not exists in the Entity." +
+ " Available column names:${allColumns.joinToString(", ")}"
+ }
+
+ val DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE = "Dao class must be an abstract class or" +
+ " an interface"
+ val DATABASE_MUST_BE_ANNOTATED_WITH_DATABASE = "Database must be annotated with @Database"
+ val DAO_MUST_BE_ANNOTATED_WITH_DAO = "Dao class must be annotated with @Dao"
+
+ fun daoMustHaveMatchingConstructor(daoName: String, dbName: String): String {
+ return """
+ $daoName needs to have either an empty constructor or a constructor that takes
+ $dbName as its only parameter.
+ """.trim()
+ }
+
+ val ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY = "Entity class must be annotated with @Entity"
+ val DATABASE_ANNOTATION_MUST_HAVE_LIST_OF_ENTITIES = "@Database annotation must specify list" +
+ " of entities"
+ val COLUMN_NAME_CANNOT_BE_EMPTY = "Column name cannot be blank. If you don't want to set it" +
+ ", just remove the @ColumnInfo annotation or use @ColumnInfo.INHERIT_FIELD_NAME."
+
+ val ENTITY_TABLE_NAME_CANNOT_BE_EMPTY = "Entity table name cannot be blank. If you don't want" +
+ " to set it, just remove the tableName property."
+
+ val CANNOT_BIND_QUERY_PARAMETER_INTO_STMT = "Query method parameters should either be a" +
+ " type that can be converted into a database column or a List / Array that contains" +
+ " such type. You can consider adding a Type Adapter for this."
+
+ val QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE = "Query/Insert method parameters cannot " +
+ "start with underscore (_)."
+
+ val CANNOT_FIND_QUERY_RESULT_ADAPTER = "Not sure how to convert a Cursor to this method's " +
+ "return type"
+
+ val INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT = "Method annotated with" +
+ " @Insert but does not have any parameters to insert."
+
+ val DELETION_MISSING_PARAMS = "Method annotated with" +
+ " @Delete but does not have any parameters to delete."
+
+ val UPDATE_MISSING_PARAMS = "Method annotated with" +
+ " @Update but does not have any parameters to update."
+
+ val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
+ "private, final, or abstract. It can be abstract only if the method is also" +
+ " annotated with @Query."
+
+ val TRANSACTION_MISSING_ON_RELATION = "The return value includes a Pojo with a @Relation." +
+ " It is usually desired to annotate this method with @Transaction to avoid" +
+ " possibility of inconsistent results between the Pojo and its relations. See " +
+ TRANSACTION_REFERENCE_DOCS + " for details."
+
+ val CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER = "Type of the parameter must be a class " +
+ "annotated with @Entity or a collection/array of it."
+
+ val DB_MUST_EXTEND_ROOM_DB = "Classes annotated with @Database should extend " +
+ RoomTypeNames.ROOM_DB
+
+ val LIVE_DATA_QUERY_WITHOUT_SELECT = "LiveData return type can only be used with SELECT" +
+ " queries."
+
+ val OBSERVABLE_QUERY_NOTHING_TO_OBSERVE = "Observable query return type (LiveData, Flowable" +
+ ", DataSource, DataSourceFactory etc) can only be used with SELECT queries that" +
+ " directly or indirectly (via @Relation, for example) access at least one table. For" +
+ " @RawQuery, you should specify the list of tables to be observed via the" +
+ " observedEntities field."
+
+ val RECURSIVE_REFERENCE_DETECTED = "Recursive referencing through @Embedded and/or @Relation " +
+ "detected: %s"
+
+ private val TOO_MANY_MATCHING_GETTERS = "Ambiguous getter for %s. All of the following " +
+ "match: %s. You can @Ignore the ones that you don't want to match."
+
+ fun tooManyMatchingGetters(field: Field, methodNames: List<String>): String {
+ return TOO_MANY_MATCHING_GETTERS.format(field, methodNames.joinToString(", "))
+ }
+
+ private val TOO_MANY_MATCHING_SETTERS = "Ambiguous setter for %s. All of the following " +
+ "match: %s. You can @Ignore the ones that you don't want to match."
+
+ fun tooManyMatchingSetter(field: Field, methodNames: List<String>): String {
+ return TOO_MANY_MATCHING_SETTERS.format(field, methodNames.joinToString(", "))
+ }
+
+ val CANNOT_FIND_COLUMN_TYPE_ADAPTER = "Cannot figure out how to save this field into" +
+ " database. You can consider adding a type converter for it."
+
+ val CANNOT_FIND_STMT_BINDER = "Cannot figure out how to bind this field into a statement."
+
+ val CANNOT_FIND_CURSOR_READER = "Cannot figure out how to read this field from a cursor."
+
+ private val MISSING_PARAMETER_FOR_BIND = "Each bind variable in the query must have a" +
+ " matching method parameter. Cannot find method parameters for %s."
+
+ fun missingParameterForBindVariable(bindVarName: List<String>): String {
+ return MISSING_PARAMETER_FOR_BIND.format(bindVarName.joinToString(", "))
+ }
+
+ private val UNUSED_QUERY_METHOD_PARAMETER = "Unused parameter%s: %s"
+ fun unusedQueryMethodParameter(unusedParams: List<String>): String {
+ return UNUSED_QUERY_METHOD_PARAMETER.format(
+ if (unusedParams.size > 1) "s" else "",
+ unusedParams.joinToString(","))
+ }
+
+ private val DUPLICATE_TABLES = "Table name \"%s\" is used by multiple entities: %s"
+ fun duplicateTableNames(tableName: String, entityNames: List<String>): String {
+ return DUPLICATE_TABLES.format(tableName, entityNames.joinToString(", "))
+ }
+
+ val DELETION_METHODS_MUST_RETURN_VOID_OR_INT = "Deletion methods must either return void or" +
+ " return int (the number of deleted rows)."
+
+ val UPDATE_METHODS_MUST_RETURN_VOID_OR_INT = "Update methods must either return void or" +
+ " return int (the number of updated rows)."
+
+ val DAO_METHOD_CONFLICTS_WITH_OTHERS = "Dao method has conflicts."
+
+ fun duplicateDao(dao: TypeName, methodNames: List<String>): String {
+ return """
+ All of these functions [${methodNames.joinToString(", ")}] return the same DAO
+ class [$dao].
+ A database can use a DAO only once so you should remove ${methodNames.size - 1} of
+ these conflicting DAO methods. If you are implementing any of these to fulfill an
+ interface, don't make it abstract, instead, implement the code that calls the
+ other one.
+ """.trim()
+ }
+
+ fun pojoMissingNonNull(pojoTypeName: TypeName, missingPojoFields: List<String>,
+ allQueryColumns: List<String>): String {
+ return """
+ The columns returned by the query does not have the fields
+ [${missingPojoFields.joinToString(",")}] in $pojoTypeName even though they are
+ annotated as non-null or primitive.
+ Columns returned by the query: [${allQueryColumns.joinToString(",")}]
+ """.trim()
+ }
+
+ fun cursorPojoMismatch(pojoTypeName: TypeName,
+ unusedColumns: List<String>, allColumns: List<String>,
+ unusedFields: List<Field>, allFields: List<Field>): String {
+ val unusedColumnsWarning = if (unusedColumns.isNotEmpty()) {
+ """
+ The query returns some columns [${unusedColumns.joinToString(", ")}] which are not
+ use by $pojoTypeName. You can use @ColumnInfo annotation on the fields to specify
+ the mapping.
+ """.trim()
+ } else {
+ ""
+ }
+ val unusedFieldsWarning = if (unusedFields.isNotEmpty()) {
+ """
+ $pojoTypeName has some fields
+ [${unusedFields.joinToString(", ") { it.columnName }}] which are not returned by the
+ query. If they are not supposed to be read from the result, you can mark them with
+ @Ignore annotation.
+ """.trim()
+ } else {
+ ""
+ }
+ return """
+ $unusedColumnsWarning
+ $unusedFieldsWarning
+ You can suppress this warning by annotating the method with
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH).
+ Columns returned by the query: ${allColumns.joinToString(", ")}.
+ Fields in $pojoTypeName: ${allFields.joinToString(", ") { it.columnName }}.
+ """.trim()
+ }
+
+ val TYPE_CONVERTER_UNBOUND_GENERIC = "Cannot use unbound generics in Type Converters."
+ val TYPE_CONVERTER_BAD_RETURN_TYPE = "Invalid return type for a type converter."
+ val TYPE_CONVERTER_MUST_RECEIVE_1_PARAM = "Type converters must receive 1 parameter."
+ val TYPE_CONVERTER_EMPTY_CLASS = "Class is referenced as a converter but it does not have any" +
+ " converter methods."
+ val TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR = "Classes that are used as TypeConverters must" +
+ " have no-argument public constructors."
+ val TYPE_CONVERTER_MUST_BE_PUBLIC = "Type converters must be public."
+
+ fun duplicateTypeConverters(converters: List<CustomTypeConverter>): String {
+ return "Multiple methods define the same conversion. Conflicts with these:" +
+ " ${converters.joinToString(", ") { it.toString() }}"
+ }
+
+ // TODO must print field paths.
+ val POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME = "Field has non-unique column name."
+
+ fun pojoDuplicateFieldNames(columnName: String, fieldPaths: List<String>): String {
+ return "Multiple fields have the same columnName: $columnName." +
+ " Field names: ${fieldPaths.joinToString(", ")}."
+ }
+
+ fun embeddedPrimaryKeyIsDropped(entityQName: String, fieldName: String): String {
+ return "Primary key constraint on $fieldName is ignored when being merged into " +
+ entityQName
+ }
+
+ val INDEX_COLUMNS_CANNOT_BE_EMPTY = "List of columns in an index cannot be empty"
+
+ fun indexColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
+ return "$columnName referenced in the index does not exists in the Entity." +
+ " Available column names:${allColumns.joinToString(", ")}"
+ }
+
+ fun duplicateIndexInEntity(indexName: String): String {
+ return "There are multiple indices with name $indexName. This happen if you've declared" +
+ " the same index multiple times or different indices have the same name. See" +
+ " @Index documentation for details."
+ }
+
+ fun duplicateIndexInDatabase(indexName: String, indexPaths: List<String>): String {
+ return "There are multiple indices with name $indexName. You should rename " +
+ "${indexPaths.size - 1} of these to avoid the conflict:" +
+ "${indexPaths.joinToString(", ")}."
+ }
+
+ fun droppedEmbeddedFieldIndex(fieldPath: String, grandParent: String): String {
+ return "The index will be dropped when being merged into $grandParent" +
+ "($fieldPath). You must re-declare it in $grandParent if you want to index this" +
+ " field in $grandParent."
+ }
+
+ fun droppedEmbeddedIndex(entityName: String, fieldPath: String, grandParent: String): String {
+ return "Indices defined in $entityName will be dropped when it is merged into" +
+ " $grandParent ($fieldPath). You can re-declare them in $grandParent."
+ }
+
+ fun droppedSuperClassIndex(childEntity: String, superEntity: String): String {
+ return "Indices defined in $superEntity will NOT be re-used in $childEntity. If you want" +
+ " to inherit them, you must re-declare them in $childEntity." +
+ " Alternatively, you can set inheritSuperIndices to true in the @Entity annotation."
+ }
+
+ fun droppedSuperClassFieldIndex(fieldName: String, childEntity: String,
+ superEntity: String): String {
+ return "Index defined on field `$fieldName` in $superEntity will NOT be re-used in" +
+ " $childEntity. " +
+ "If you want to inherit it, you must re-declare it in $childEntity." +
+ " Alternatively, you can set inheritSuperIndices to true in the @Entity annotation."
+ }
+
+ val RELATION_NOT_COLLECTION = "Fields annotated with @Relation must be a List or Set."
+
+ fun relationCannotFindEntityField(entityName: String, columnName: String,
+ availableColumns: List<String>): String {
+ return "Cannot find the child entity column `$columnName` in $entityName." +
+ " Options: ${availableColumns.joinToString(", ")}"
+ }
+
+ fun relationCannotFindParentEntityField(entityName: String, columnName: String,
+ availableColumns: List<String>): String {
+ return "Cannot find the parent entity column `$columnName` in $entityName." +
+ " Options: ${availableColumns.joinToString(", ")}"
+ }
+
+ val RELATION_IN_ENTITY = "Entities cannot have relations."
+
+ val CANNOT_FIND_TYPE = "Cannot find type."
+
+ fun relationAffinityMismatch(parentColumn: String, childColumn: String,
+ parentAffinity: SQLTypeAffinity?,
+ childAffinity: SQLTypeAffinity?): String {
+ return """
+ The affinity of parent column ($parentColumn : $parentAffinity) does not match the type
+ affinity of the child column ($childColumn : $childAffinity).
+ """.trim()
+ }
+
+ val CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION = "A field can be annotated with only" +
+ " one of the following:" + PojoProcessor.PROCESSED_ANNOTATIONS.joinToString(",") {
+ it.java.simpleName
+ }
+
+ fun relationBadProject(entityQName: String, missingColumnNames: List<String>,
+ availableColumnNames: List<String>): String {
+ return """
+ $entityQName does not have the following columns: ${missingColumnNames.joinToString(",")}.
+ Available columns are: ${availableColumnNames.joinToString(",")}
+ """.trim()
+ }
+
+ val MISSING_SCHEMA_EXPORT_DIRECTORY = "Schema export directory is not provided to the" +
+ " annotation processor so we cannot export the schema. You can either provide" +
+ " `room.schemaLocation` annotation processor argument OR set exportSchema to false."
+
+ val INVALID_FOREIGN_KEY_ACTION = "Invalid foreign key action. It must be one of the constants" +
+ " defined in ForeignKey.Action"
+
+ fun foreignKeyNotAnEntity(className: String): String {
+ return """
+ Classes referenced in Foreign Key annotations must be @Entity classes. $className is not
+ an entity
+ """.trim()
+ }
+
+ val FOREIGN_KEY_CANNOT_FIND_PARENT = "Cannot find parent entity class."
+
+ fun foreignKeyChildColumnDoesNotExist(columnName: String, allColumns: List<String>): String {
+ return "($columnName) referenced in the foreign key does not exists in the Entity." +
+ " Available column names:${allColumns.joinToString(", ")}"
+ }
+
+ fun foreignKeyParentColumnDoesNotExist(parentEntity: String,
+ missingColumn: String,
+ allColumns: List<String>): String {
+ return "($missingColumn) does not exist in $parentEntity. Available columns are" +
+ " ${allColumns.joinToString(",")}"
+ }
+
+ val FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST = "Must specify at least 1 column name for the child"
+
+ val FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST = "Must specify at least 1 column name for the parent"
+
+ fun foreignKeyColumnNumberMismatch(
+ childColumns: List<String>, parentColumns: List<String>): String {
+ return """
+ Number of child columns in foreign key must match number of parent columns.
+ Child reference has ${childColumns.joinToString(",")} and parent reference has
+ ${parentColumns.joinToString(",")}
+ """.trim()
+ }
+
+ fun foreignKeyMissingParentEntityInDatabase(parentTable: String, childEntity: String): String {
+ return """
+ $parentTable table referenced in the foreign keys of $childEntity does not exist in
+ the database. Maybe you forgot to add the referenced entity in the entities list of
+ the @Database annotation?""".trim()
+ }
+
+ fun foreignKeyMissingIndexInParent(parentEntity: String, parentColumns: List<String>,
+ childEntity: String, childColumns: List<String>): String {
+ return """
+ $childEntity has a foreign key (${childColumns.joinToString(",")}) that references
+ $parentEntity (${parentColumns.joinToString(",")}) but $parentEntity does not have
+ a unique index on those columns nor the columns are its primary key.
+ SQLite requires having a unique constraint on referenced parent columns so you must
+ add a unique index to $parentEntity that has
+ (${parentColumns.joinToString(",")}) column(s).
+ """.trim()
+ }
+
+ fun foreignKeyMissingIndexInChildColumns(childColumns: List<String>): String {
+ return """
+ (${childColumns.joinToString(",")}) column(s) reference a foreign key but
+ they are not part of an index. This may trigger full table scans whenever parent
+ table is modified so you are highly advised to create an index that covers these
+ columns.
+ """.trim()
+ }
+
+ fun foreignKeyMissingIndexInChildColumn(childColumn: String): String {
+ return """
+ $childColumn column references a foreign key but it is not part of an index. This
+ may trigger full table scans whenever parent table is modified so you are highly
+ advised to create an index that covers this column.
+ """.trim()
+ }
+
+ fun shortcutEntityIsNotInDatabase(database: String, dao: String, entity: String): String {
+ return """
+ $dao is part of $database but this entity is not in the database. Maybe you forgot
+ to add $entity to the entities section of the @Database?
+ """.trim()
+ }
+
+ val MISSING_ROOM_GUAVA_ARTIFACT = "To use Guava features, you must add `guava`" +
+ " artifact from Room as a dependency. androidx.room:guava:<version>"
+
+ val MISSING_ROOM_RXJAVA2_ARTIFACT = "To use RxJava2 features, you must add `rxjava2`" +
+ " artifact from Room as a dependency. androidx.room:rxjava2:<version>"
+
+ fun ambigiousConstructor(
+ pojo: String, paramName: String, matchingFields: List<String>): String {
+ return """
+ Ambiguous constructor. The parameter ($paramName) in $pojo matches multiple fields:
+ [${matchingFields.joinToString(",")}]. If you don't want to use this constructor,
+ you can annotate it with @Ignore. If you want Room to use this constructor, you can
+ rename the parameters to exactly match the field name to fix the ambiguity.
+ """.trim()
+ }
+
+ val MISSING_POJO_CONSTRUCTOR = """
+ Entities and Pojos must have a usable public constructor. You can have an empty
+ constructor or a constructor whose parameters match the fields (by name and type).
+ """.trim()
+
+ val TOO_MANY_POJO_CONSTRUCTORS = """
+ Room cannot pick a constructor since multiple constructors are suitable. Try to annotate
+ unwanted constructors with @Ignore.
+ """.trim()
+
+ val TOO_MANY_POJO_CONSTRUCTORS_CHOOSING_NO_ARG = """
+ There are multiple good constructors and Room will pick the no-arg constructor.
+ You can use the @Ignore annotation to eliminate unwanted constructors.
+ """.trim()
+
+ val RELATION_CANNOT_BE_CONSTRUCTOR_PARAMETER = """
+ Fields annotated with @Relation cannot be constructor parameters. These values are
+ fetched after the object is constructed.
+ """.trim()
+
+ val PAGING_SPECIFY_DATA_SOURCE_TYPE = "For now, Room only supports PositionalDataSource class."
+
+ fun primaryKeyNull(field: String): String {
+ return "You must annotate primary keys with @NonNull. \"$field\" is nullable. SQLite " +
+ "considers this a " +
+ "bug and Room does not allow it. See SQLite docs for details: " +
+ "https://www.sqlite.org/lang_createtable.html"
+ }
+
+ val INVALID_COLUMN_NAME = "Invalid column name. Room does not allow using ` or \" in column" +
+ " names"
+
+ val INVALID_TABLE_NAME = "Invalid table name. Room does not allow using ` or \" in table names"
+
+ val RAW_QUERY_BAD_PARAMS = "RawQuery methods should have 1 and only 1 parameter with type" +
+ " String or SupportSQLiteQuery"
+
+ val RAW_QUERY_BAD_RETURN_TYPE = "RawQuery methods must return a non-void type."
+
+ fun rawQueryBadEntity(typeName: TypeName): String {
+ return """
+ observedEntities field in RawQuery must either reference a class that is annotated
+ with @Entity or it should reference a Pojo that either contains @Embedded fields that
+ are annotated with @Entity or @Relation fields.
+ $typeName does not have these properties, did you mean another class?
+ """.trim()
+ }
+
+ val RAW_QUERY_STRING_PARAMETER_REMOVED = "RawQuery does not allow passing a string anymore." +
+ " Please use ${SupportDbTypeNames.QUERY}."
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
new file mode 100644
index 0000000..6773cac
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Query
+import androidx.room.SkipQueryVerification
+import androidx.room.Transaction
+import androidx.room.ext.KotlinMetadataProcessor
+import androidx.room.ext.hasAnnotation
+import androidx.room.parser.ParsedQuery
+import androidx.room.parser.QueryType
+import androidx.room.parser.SqlParser
+import androidx.room.solver.query.result.LiveDataQueryResultBinder
+import androidx.room.solver.query.result.PojoRowAdapter
+import androidx.room.verifier.DatabaseVerificaitonErrors
+import androidx.room.verifier.DatabaseVerifier
+import androidx.room.vo.QueryMethod
+import androidx.room.vo.QueryParameter
+import androidx.room.vo.Warning
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.TypeName
+import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
+import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+
+class QueryMethodProcessor(
+ baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement,
+ val dbVerifier: DatabaseVerifier? = null
+) : KotlinMetadataProcessor {
+ val context = baseContext.fork(executableElement)
+
+ // for kotlin metadata
+ override val processingEnv: ProcessingEnvironment
+ get() = context.processingEnv
+
+ private val classMetadata =
+ try {
+ containing.asElement().kotlinMetadata
+ } catch (throwable: Throwable) {
+ context.logger.d(executableElement,
+ "failed to read get kotlin metadata from %s", executableElement)
+ } as? KotlinClassMetadata
+
+ fun process(): QueryMethod {
+ val asMember = context.processingEnv.typeUtils.asMemberOf(containing, executableElement)
+ val executableType = MoreTypes.asExecutable(asMember)
+
+ val annotation = MoreElements.getAnnotationMirror(executableElement,
+ Query::class.java).orNull()
+ context.checker.check(annotation != null, executableElement,
+ ProcessorErrors.MISSING_QUERY_ANNOTATION)
+
+ val query = if (annotation != null) {
+ val query = SqlParser.parse(
+ AnnotationMirrors.getAnnotationValue(annotation, "value").value.toString())
+ context.checker.check(query.errors.isEmpty(), executableElement,
+ query.errors.joinToString("\n"))
+ if (!executableElement.hasAnnotation(SkipQueryVerification::class)) {
+ query.resultInfo = dbVerifier?.analyze(query.original)
+ }
+ if (query.resultInfo?.error != null) {
+ context.logger.e(executableElement,
+ DatabaseVerificaitonErrors.cannotVerifyQuery(query.resultInfo!!.error!!))
+ }
+
+ context.checker.check(executableType.returnType.kind != TypeKind.ERROR,
+ executableElement, ProcessorErrors.CANNOT_RESOLVE_RETURN_TYPE,
+ executableElement)
+ query
+ } else {
+ ParsedQuery.MISSING
+ }
+
+ val returnTypeName = TypeName.get(executableType.returnType)
+ context.checker.notUnbound(returnTypeName, executableElement,
+ ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
+
+ if (query.type == QueryType.DELETE) {
+ context.checker.check(
+ returnTypeName == TypeName.VOID || returnTypeName == TypeName.INT,
+ executableElement,
+ ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
+ )
+ }
+ val resultBinder = context.typeAdapterStore
+ .findQueryResultBinder(executableType.returnType, query)
+ context.checker.check(resultBinder.adapter != null || query.type != QueryType.SELECT,
+ executableElement, ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ if (resultBinder is LiveDataQueryResultBinder) {
+ context.checker.check(query.type == QueryType.SELECT, executableElement,
+ ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
+ }
+
+ val inTransaction = when (query.type) {
+ QueryType.SELECT -> executableElement.hasAnnotation(Transaction::class)
+ else -> true
+ }
+
+ if (query.type == QueryType.SELECT && !inTransaction) {
+ // put a warning if it is has relations and not annotated w/ transaction
+ resultBinder.adapter?.rowAdapter?.let { rowAdapter ->
+ if (rowAdapter is PojoRowAdapter
+ && rowAdapter.relationCollectors.isNotEmpty()) {
+ context.logger.w(Warning.RELATION_QUERY_WITHOUT_TRANSACTION,
+ executableElement, ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ }
+ }
+ }
+ val kotlinParameterNames = classMetadata?.getParameterNames(executableElement)
+
+ val parameters = executableElement.parameters
+ .mapIndexed { index, variableElement ->
+ QueryParameterProcessor(
+ baseContext = context,
+ containing = containing,
+ element = variableElement,
+ sqlName = kotlinParameterNames?.getOrNull(index)).process()
+ }
+ val queryMethod = QueryMethod(
+ element = executableElement,
+ query = query,
+ name = executableElement.simpleName.toString(),
+ returnType = executableType.returnType,
+ parameters = parameters,
+ inTransaction = inTransaction,
+ queryResultBinder = resultBinder)
+
+ val missing = queryMethod.sectionToParamMapping
+ .filter { it.second == null }
+ .map { it.first.text }
+ if (missing.isNotEmpty()) {
+ context.logger.e(executableElement,
+ ProcessorErrors.missingParameterForBindVariable(missing))
+ }
+
+ val unused = queryMethod.parameters.filterNot { param ->
+ queryMethod.sectionToParamMapping.any { it.second == param }
+ }.map(QueryParameter::sqlName)
+
+ if (unused.isNotEmpty()) {
+ context.logger.e(executableElement, ProcessorErrors.unusedQueryMethodParameter(unused))
+ }
+ return queryMethod
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt
new file mode 100644
index 0000000..13fb3a1
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryParameterProcessor.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.vo.QueryParameter
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.DeclaredType
+
+class QueryParameterProcessor(
+ baseContext: Context,
+ val containing: DeclaredType,
+ val element: VariableElement,
+ private val sqlName: String? = null) {
+ val context = baseContext.fork(element)
+ fun process(): QueryParameter {
+ val asMember = MoreTypes.asMemberOf(context.processingEnv.typeUtils, containing, element)
+ val parameterAdapter = context.typeAdapterStore.findQueryParameterAdapter(asMember)
+ context.checker.check(parameterAdapter != null, element,
+ ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
+
+ val name = element.simpleName.toString()
+ context.checker.check(!name.startsWith("_"), element,
+ ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
+ return QueryParameter(
+ name = name,
+ sqlName = sqlName ?: name,
+ type = asMember,
+ queryParamAdapter = parameterAdapter)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
new file mode 100644
index 0000000..128c0f1
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2018 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 androidx.room.processor
+
+import androidx.room.RawQuery
+import androidx.room.Transaction
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.toListOfClassTypes
+import androidx.room.ext.typeName
+import androidx.room.parser.SqlParser
+import androidx.room.processor.ProcessorErrors.RAW_QUERY_STRING_PARAMETER_REMOVED
+import androidx.room.vo.RawQueryMethod
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+class RawQueryMethodProcessor(
+ baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+ val context = baseContext.fork(executableElement)
+ fun process(): RawQueryMethod {
+ val types = context.processingEnv.typeUtils
+ val asMember = types.asMemberOf(containing, executableElement)
+ val executableType = MoreTypes.asExecutable(asMember)
+
+ val annotation = MoreElements.getAnnotationMirror(executableElement,
+ RawQuery::class.java).orNull()
+ context.checker.check(annotation != null, executableElement,
+ ProcessorErrors.MISSING_RAWQUERY_ANNOTATION)
+
+ val returnTypeName = TypeName.get(executableType.returnType)
+ context.checker.notUnbound(returnTypeName, executableElement,
+ ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
+ val observedTableNames = processObservedTables()
+ val query = SqlParser.rawQueryForTables(observedTableNames)
+ // build the query but don't calculate result info since we just guessed it.
+ val resultBinder = context.typeAdapterStore
+ .findQueryResultBinder(executableType.returnType, query)
+
+ val runtimeQueryParam = findRuntimeQueryParameter()
+ val inTransaction = executableElement.hasAnnotation(Transaction::class)
+ val rawQueryMethod = RawQueryMethod(
+ element = executableElement,
+ name = executableElement.simpleName.toString(),
+ observedTableNames = observedTableNames,
+ returnType = executableType.returnType,
+ runtimeQueryParam = runtimeQueryParam,
+ inTransaction = inTransaction,
+ queryResultBinder = resultBinder
+ )
+ context.checker.check(rawQueryMethod.returnsValue, executableElement,
+ ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE)
+ return rawQueryMethod
+ }
+
+ private fun processObservedTables(): Set<String> {
+ val annotation = MoreElements
+ .getAnnotationMirror(executableElement,
+ androidx.room.RawQuery::class.java)
+ .orNull() ?: return emptySet()
+ val entityList = AnnotationMirrors.getAnnotationValue(annotation, "observedEntities")
+ return entityList
+ .toListOfClassTypes()
+ .map {
+ MoreTypes.asTypeElement(it)
+ }
+ .flatMap {
+ if (it.hasAnnotation(androidx.room.Entity::class)) {
+ val entity = EntityProcessor(
+ baseContext = context,
+ element = it
+ ).process()
+ arrayListOf(entity.tableName)
+ } else {
+ val pojo = PojoProcessor(
+ baseContext = context,
+ element = it,
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = null
+ ).process()
+ val tableNames = pojo.accessedTableNames()
+ // if it is empty, report error as it does not make sense
+ if (tableNames.isEmpty()) {
+ context.logger.e(executableElement,
+ ProcessorErrors.rawQueryBadEntity(it.asType().typeName()))
+ }
+ tableNames
+ }
+ }.toSet()
+ }
+
+ private fun findRuntimeQueryParameter(): RawQueryMethod.RuntimeQueryParameter? {
+ val types = context.processingEnv.typeUtils
+ if (executableElement.parameters.size == 1 && !executableElement.isVarArgs) {
+ val param = MoreTypes.asMemberOf(
+ types,
+ containing,
+ executableElement.parameters[0])
+ val elementUtils = context.processingEnv.elementUtils
+ val supportQueryType = elementUtils
+ .getTypeElement(SupportDbTypeNames.QUERY.toString()).asType()
+ val isSupportSql = types.isAssignable(param, supportQueryType)
+ if (isSupportSql) {
+ return RawQueryMethod.RuntimeQueryParameter(
+ paramName = executableElement.parameters[0].simpleName.toString(),
+ type = supportQueryType.typeName())
+ }
+ val stringType = elementUtils.getTypeElement("java.lang.String").asType()
+ val isString = types.isAssignable(param, stringType)
+ if (isString) {
+ // special error since this was initially allowed but removed in 1.1 beta1
+ context.logger.e(executableElement, RAW_QUERY_STRING_PARAMETER_REMOVED)
+ return null
+ }
+ }
+ context.logger.e(executableElement, ProcessorErrors.RAW_QUERY_BAD_PARAMS)
+ return null
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
new file mode 100644
index 0000000..45c6455
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.vo.Entity
+import androidx.room.vo.ShortcutQueryParameter
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+import kotlin.reflect.KClass
+
+/**
+ * Common functionality for shortcut method processors
+ */
+class ShortcutMethodProcessor(baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+ val context = baseContext.fork(executableElement)
+ private val asMember = context.processingEnv.typeUtils.asMemberOf(containing, executableElement)
+ private val executableType = MoreTypes.asExecutable(asMember)
+
+ fun extractAnnotation(klass: KClass<out Annotation>,
+ errorMsg: String): AnnotationMirror? {
+ val annotation = MoreElements.getAnnotationMirror(executableElement,
+ klass.java).orNull()
+ context.checker.check(annotation != null, executableElement, errorMsg)
+ return annotation
+ }
+
+ fun extractReturnType(): TypeMirror {
+ return executableType.returnType
+ }
+
+ fun extractParams(
+ missingParamError: String
+ ): Pair<Map<String, Entity>, List<ShortcutQueryParameter>> {
+ val params = executableElement.parameters
+ .map { ShortcutParameterProcessor(
+ baseContext = context,
+ containing = containing,
+ element = it).process() }
+ context.checker.check(params.isNotEmpty(), executableElement, missingParamError)
+ val entities = params
+ .filter { it.entityType != null }
+ .associateBy({ it.name }, {
+ EntityProcessor(
+ baseContext = context,
+ element = MoreTypes.asTypeElement(it.entityType)).process()
+ })
+ return Pair(entities, params)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutParameterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutParameterProcessor.kt
new file mode 100644
index 0000000..1e7c8c5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutParameterProcessor.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Entity
+import androidx.room.ext.extendsBound
+import androidx.room.ext.hasAnnotation
+import androidx.room.vo.ShortcutQueryParameter
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.util.ElementFilter
+
+/**
+ * Processes parameters of methods that are annotated with Insert, Delete.
+ */
+class ShortcutParameterProcessor(baseContext: Context,
+ val containing: DeclaredType,
+ val element: VariableElement) {
+ val context = baseContext.fork(element)
+ fun process(): ShortcutQueryParameter {
+ val asMember = MoreTypes.asMemberOf(context.processingEnv.typeUtils, containing, element)
+ val name = element.simpleName.toString()
+ context.checker.check(!name.startsWith("_"), element,
+ ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
+
+ val (entityType, isMultiple) = extractEntityType(asMember)
+ context.checker.check(entityType != null, element,
+ ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER)
+
+ return ShortcutQueryParameter(
+ name = name,
+ type = asMember,
+ entityType = entityType,
+ isMultiple = isMultiple
+ )
+ }
+
+ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ fun extractEntityType(typeMirror: TypeMirror): Pair<TypeMirror?, Boolean> {
+
+ val elementUtils = context.processingEnv.elementUtils
+ val typeUtils = context.processingEnv.typeUtils
+
+ fun verifyAndPair(entityType: TypeMirror, isMultiple: Boolean): Pair<TypeMirror?, Boolean> {
+ if (!MoreTypes.isType(entityType)) {
+ // kotlin may generate ? extends T so we should reduce it.
+ val boundedVar = entityType.extendsBound()
+ return boundedVar?.let {
+ verifyAndPair(boundedVar, isMultiple)
+ } ?: Pair(null, isMultiple)
+ }
+ val entityElement = MoreTypes.asElement(entityType)
+ return if (entityElement.hasAnnotation(Entity::class)) {
+ Pair(entityType, isMultiple)
+ } else {
+ Pair(null, isMultiple)
+ }
+ }
+
+ fun extractEntityTypeFromIterator(iterableType: DeclaredType): TypeMirror {
+ ElementFilter.methodsIn(elementUtils
+ .getAllMembers(typeUtils.asElement(iterableType) as TypeElement)).forEach {
+ if (it.simpleName.toString() == "iterator") {
+ return MoreTypes.asDeclared(MoreTypes.asExecutable(
+ typeUtils.asMemberOf(iterableType, it)).returnType)
+ .typeArguments.first()
+ }
+ }
+ throw IllegalArgumentException("iterator() not found in Iterable $iterableType")
+ }
+
+ val iterableType = typeUtils.erasure(elementUtils
+ .getTypeElement("java.lang.Iterable").asType())
+ if (typeUtils.isAssignable(typeMirror, iterableType)) {
+ val declared = MoreTypes.asDeclared(typeMirror)
+ val entity = extractEntityTypeFromIterator(declared)
+ return verifyAndPair(entity, true)
+ }
+ if (typeMirror is ArrayType) {
+ val entity = typeMirror.componentType
+ return verifyAndPair(entity, true)
+ }
+ return verifyAndPair(typeMirror, false)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
new file mode 100644
index 0000000..eb2cde8
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
@@ -0,0 +1,58 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.vo.Warning
+import com.google.auto.common.AnnotationMirrors
+import com.google.auto.common.MoreElements
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.util.SimpleAnnotationValueVisitor6
+
+/**
+ * A visitor that reads SuppressWarnings annotations and keeps the ones we know about.
+ */
+object SuppressWarningProcessor {
+
+ fun getSuppressedWarnings(element: Element): Set<Warning> {
+ val annotation = MoreElements.getAnnotationMirror(element,
+ SuppressWarnings::class.java).orNull()
+ return if (annotation == null) {
+ emptySet<Warning>()
+ } else {
+ val value = AnnotationMirrors.getAnnotationValue(annotation, "value")
+ if (value == null) {
+ emptySet<Warning>()
+ } else {
+ VISITOR.visit(value)
+ }
+ }
+ }
+
+ private object VISITOR : SimpleAnnotationValueVisitor6<Set<Warning>, String>() {
+ override fun visitArray(values: List<AnnotationValue>?, elementName: String?
+ ): Set<Warning> {
+ return values?.map {
+ Warning.fromPublicKey(it.value.toString())
+ }?.filterNotNull()?.toSet() ?: emptySet()
+ }
+
+ override fun defaultAction(o: Any?, p: String?): Set<Warning> {
+ return emptySet()
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
new file mode 100644
index 0000000..a309e8e
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
@@ -0,0 +1,57 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.ext.findKotlinDefaultImpl
+import androidx.room.ext.hasAnyOf
+import androidx.room.vo.TransactionMethod
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier.ABSTRACT
+import javax.lang.model.element.Modifier.DEFAULT
+import javax.lang.model.element.Modifier.FINAL
+import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.type.DeclaredType
+
+class TransactionMethodProcessor(baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+
+ val context = baseContext.fork(executableElement)
+
+ fun process(): TransactionMethod {
+ val kotlinDefaultImpl =
+ executableElement.findKotlinDefaultImpl(context.processingEnv.typeUtils)
+ context.checker.check(
+ !executableElement.hasAnyOf(PRIVATE, FINAL)
+ && (!executableElement.hasAnyOf(ABSTRACT) || kotlinDefaultImpl != null),
+ executableElement, ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+
+ val callType = when {
+ executableElement.hasAnyOf(DEFAULT) ->
+ TransactionMethod.CallType.DEFAULT_JAVA8
+ kotlinDefaultImpl != null ->
+ TransactionMethod.CallType.DEFAULT_KOTLIN
+ else ->
+ TransactionMethod.CallType.CONCRETE
+ }
+
+ return TransactionMethod(
+ element = executableElement,
+ name = executableElement.simpleName.toString(),
+ callType = callType)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/UpdateMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/UpdateMethodProcessor.kt
new file mode 100644
index 0000000..f0b9b29
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/UpdateMethodProcessor.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.OnConflictStrategy.IGNORE
+import androidx.room.OnConflictStrategy.REPLACE
+import androidx.room.Update
+import androidx.room.ext.typeName
+import androidx.room.vo.UpdateMethod
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+class UpdateMethodProcessor(
+ baseContext: Context,
+ val containing: DeclaredType,
+ val executableElement: ExecutableElement) {
+ val context = baseContext.fork(executableElement)
+
+ fun process(): UpdateMethod {
+ val delegate = ShortcutMethodProcessor(context, containing, executableElement)
+ val annotation = delegate
+ .extractAnnotation(Update::class, ProcessorErrors.MISSING_UPDATE_ANNOTATION)
+
+ val onConflict = OnConflictProcessor.extractFrom(annotation)
+ context.checker.check(onConflict <= IGNORE && onConflict >= REPLACE,
+ executableElement, ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+
+ val returnTypeName = delegate.extractReturnType().typeName()
+ context.checker.check(
+ returnTypeName == TypeName.VOID || returnTypeName == TypeName.INT,
+ executableElement,
+ ProcessorErrors.UPDATE_METHODS_MUST_RETURN_VOID_OR_INT
+ )
+
+ val (entities, params) = delegate.extractParams(
+ missingParamError = ProcessorErrors
+ .UPDATE_MISSING_PARAMS
+ )
+
+ return UpdateMethod(
+ element = delegate.executableElement,
+ name = delegate.executableElement.simpleName.toString(),
+ entities = entities,
+ onConflictStrategy = onConflict,
+ returnCount = returnTypeName == TypeName.INT,
+ parameters = params
+ )
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/cache/Cache.kt b/room/compiler/src/main/kotlin/androidx/room/processor/cache/Cache.kt
new file mode 100644
index 0000000..409ad52
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/cache/Cache.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("AddVarianceModifier")
+
+package androidx.room.processor.cache
+
+import androidx.room.processor.FieldProcessor
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Entity
+import androidx.room.vo.Pojo
+import androidx.room.vo.Warning
+import java.util.LinkedHashSet
+import javax.lang.model.element.Element
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A cache key can be used to avoid re-processing elements.
+ * <p>
+ * Each context has a cache variable that uses the same backing storage as the Root Context but
+ * adds current adapters and warning suppression list to the key.
+ */
+class Cache(val parent: Cache?, val converters: LinkedHashSet<TypeMirror>,
+ val suppressedWarnings: Set<Warning>) {
+ val entities: Bucket<EntityKey, Entity> = Bucket(parent?.entities)
+ val pojos: Bucket<PojoKey, Pojo> = Bucket(parent?.pojos)
+
+ inner class Bucket<K, T>(source: Bucket<K, T>?) {
+ private val entries: MutableMap<FullKey<K>, T> = source?.entries ?: mutableMapOf()
+ fun get(key: K, calculate: () -> T): T {
+ val fullKey = FullKey(converters, suppressedWarnings, key)
+ return entries.getOrPut(fullKey, {
+ calculate()
+ })
+ }
+ }
+
+ /**
+ * Key for Entity cache
+ */
+ data class EntityKey(val element: Element)
+
+ /**
+ * Key for Pojo cache
+ */
+ data class PojoKey(
+ val element: Element,
+ val scope: FieldProcessor.BindingScope,
+ val parent: EmbeddedField?)
+
+ /**
+ * Internal key representation with adapters & warnings included.
+ * <p>
+ * Converters are kept in a linked set since the order is important for the TypeAdapterStore.
+ */
+ private data class FullKey<T>(
+ val converters: LinkedHashSet<TypeMirror>,
+ val suppressedWarnings: Set<Warning>,
+ val key: T)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt b/room/compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
new file mode 100644
index 0000000..ca6a11d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver
+
+import androidx.room.writer.ClassWriter
+import com.google.common.annotations.VisibleForTesting
+import com.squareup.javapoet.CodeBlock
+
+/**
+ * Defines a code generation scope where we can provide temporary variables, global variables etc
+ */
+class CodeGenScope(val writer: ClassWriter) {
+ private var tmpVarIndices = mutableMapOf<String, Int>()
+ private var builder: CodeBlock.Builder? = null
+ companion object {
+ const val TMP_VAR_DEFAULT_PREFIX = "_tmp"
+ const val CLASS_PROPERTY_PREFIX = "__"
+ @VisibleForTesting
+ fun _tmpVar(index: Int) = _tmpVar(TMP_VAR_DEFAULT_PREFIX, index)
+ fun _tmpVar(prefix: String, index: Int) = "$prefix${if (index == 0) "" else "_$index"}"
+ }
+
+ fun builder(): CodeBlock.Builder {
+ if (builder == null) {
+ builder = CodeBlock.builder()
+ }
+ return builder!!
+ }
+
+ fun getTmpVar(): String {
+ return getTmpVar(TMP_VAR_DEFAULT_PREFIX)
+ }
+
+ fun getTmpVar(prefix: String): String {
+ if (!prefix.startsWith("_")) {
+ throw IllegalArgumentException("tmp variable prefixes should start with _")
+ }
+ if (prefix.startsWith(CLASS_PROPERTY_PREFIX)) {
+ throw IllegalArgumentException("cannot use $CLASS_PROPERTY_PREFIX for tmp variables")
+ }
+ val index = tmpVarIndices.getOrElse(prefix) { 0 }
+ val result = _tmpVar(prefix, index)
+ tmpVarIndices.put(prefix, index + 1)
+ return result
+ }
+
+ fun generate() = builder().build().toString()
+
+ /**
+ * copies all variable indices but excludes generated code.
+ */
+ fun fork(): CodeGenScope {
+ val forked = CodeGenScope(writer)
+ forked.tmpVarIndices.putAll(tmpVarIndices)
+ return forked
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/ObservableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/ObservableQueryResultBinderProvider.kt
new file mode 100644
index 0000000..be38212
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/ObservableQueryResultBinderProvider.kt
@@ -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 androidx.room.solver
+
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.query.result.QueryResultAdapter
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Binder provider class that has common functionality for observables.
+ */
+abstract class ObservableQueryResultBinderProvider(val context: Context)
+ : QueryResultBinderProvider {
+ protected abstract fun extractTypeArg(declared: DeclaredType): TypeMirror
+ protected abstract fun create(typeArg: TypeMirror,
+ resultAdapter: QueryResultAdapter?,
+ tableNames: Set<String>): QueryResultBinder
+
+ final override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ val typeArg = extractTypeArg(declared)
+ val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
+ val tableNames = ((adapter?.accessedTableNames() ?: emptyList()) +
+ query.tables.map { it.name }).toSet()
+ if (tableNames.isEmpty()) {
+ context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+ return create(
+ typeArg = typeArg,
+ resultAdapter = adapter,
+ tableNames = tableNames
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/QueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/QueryResultBinderProvider.kt
new file mode 100644
index 0000000..2fda890
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/QueryResultBinderProvider.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 androidx.room.solver
+
+import androidx.room.parser.ParsedQuery
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+
+interface QueryResultBinderProvider {
+ fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder
+ fun matches(declared: DeclaredType): Boolean
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
new file mode 100644
index 0000000..322bf8d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver
+
+import androidx.room.Entity
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.typeName
+import androidx.room.parser.ParsedQuery
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.processor.Context
+import androidx.room.processor.EntityProcessor
+import androidx.room.processor.FieldProcessor
+import androidx.room.processor.PojoProcessor
+import androidx.room.solver.binderprovider.CursorQueryResultBinderProvider
+import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
+import androidx.room.solver.binderprovider.DataSourceQueryResultBinderProvider
+import androidx.room.solver.binderprovider.FlowableQueryResultBinderProvider
+import androidx.room.solver.binderprovider.GuavaListenableFutureQueryResultBinderProvider
+import androidx.room.solver.binderprovider.InstantQueryResultBinderProvider
+import androidx.room.solver.binderprovider.LiveDataQueryResultBinderProvider
+import androidx.room.solver.binderprovider.RxMaybeQueryResultBinderProvider
+import androidx.room.solver.binderprovider.RxSingleQueryResultBinderProvider
+import androidx.room.solver.query.parameter.ArrayQueryParameterAdapter
+import androidx.room.solver.query.parameter.BasicQueryParameterAdapter
+import androidx.room.solver.query.parameter.CollectionQueryParameterAdapter
+import androidx.room.solver.query.parameter.QueryParameterAdapter
+import androidx.room.solver.query.result.ArrayQueryResultAdapter
+import androidx.room.solver.query.result.EntityRowAdapter
+import androidx.room.solver.query.result.GuavaOptionalQueryResultAdapter
+import androidx.room.solver.query.result.InstantQueryResultBinder
+import androidx.room.solver.query.result.ListQueryResultAdapter
+import androidx.room.solver.query.result.OptionalQueryResultAdapter
+import androidx.room.solver.query.result.PojoRowAdapter
+import androidx.room.solver.query.result.QueryResultAdapter
+import androidx.room.solver.query.result.QueryResultBinder
+import androidx.room.solver.query.result.RowAdapter
+import androidx.room.solver.query.result.SingleColumnRowAdapter
+import androidx.room.solver.query.result.SingleEntityQueryResultAdapter
+import androidx.room.solver.types.BoxedBooleanToBoxedIntConverter
+import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
+import androidx.room.solver.types.ByteArrayColumnTypeAdapter
+import androidx.room.solver.types.ColumnTypeAdapter
+import androidx.room.solver.types.CompositeAdapter
+import androidx.room.solver.types.CompositeTypeConverter
+import androidx.room.solver.types.CursorValueReader
+import androidx.room.solver.types.NoOpConverter
+import androidx.room.solver.types.PrimitiveBooleanToIntConverter
+import androidx.room.solver.types.PrimitiveColumnTypeAdapter
+import androidx.room.solver.types.StatementValueBinder
+import androidx.room.solver.types.StringColumnTypeAdapter
+import androidx.room.solver.types.TypeConverter
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.annotations.VisibleForTesting
+import java.util.LinkedList
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+/**
+ * Holds all type adapters and can create on demand composite type adapters to convert a type into a
+ * database column.
+ */
+class TypeAdapterStore private constructor(
+ val context: Context,
+ /**
+ * first type adapter has the highest priority
+ */
+ private val columnTypeAdapters: List<ColumnTypeAdapter>,
+ /**
+ * first converter has the highest priority
+ */
+ private val typeConverters: List<TypeConverter>) {
+
+ companion object {
+ fun copy(context: Context, store: TypeAdapterStore): TypeAdapterStore {
+ return TypeAdapterStore(context = context,
+ columnTypeAdapters = store.columnTypeAdapters,
+ typeConverters = store.typeConverters)
+ }
+
+ fun create(context: Context, @VisibleForTesting vararg extras: Any): TypeAdapterStore {
+ val adapters = arrayListOf<ColumnTypeAdapter>()
+ val converters = arrayListOf<TypeConverter>()
+
+ fun addAny(extra: Any?) {
+ when (extra) {
+ is TypeConverter -> converters.add(extra)
+ is ColumnTypeAdapter -> adapters.add(extra)
+ is List<*> -> extra.forEach(::addAny)
+ else -> throw IllegalArgumentException("unknown extra $extra")
+ }
+ }
+
+ extras.forEach(::addAny)
+ fun addTypeConverter(converter: TypeConverter) {
+ converters.add(converter)
+ }
+
+ fun addColumnAdapter(adapter: ColumnTypeAdapter) {
+ adapters.add(adapter)
+ }
+
+ val primitives = PrimitiveColumnTypeAdapter
+ .createPrimitiveAdapters(context.processingEnv)
+ primitives.forEach(::addColumnAdapter)
+ BoxedPrimitiveColumnTypeAdapter
+ .createBoxedPrimitiveAdapters(context.processingEnv, primitives)
+ .forEach(::addColumnAdapter)
+ addColumnAdapter(StringColumnTypeAdapter(context.processingEnv))
+ addColumnAdapter(ByteArrayColumnTypeAdapter(context.processingEnv))
+ PrimitiveBooleanToIntConverter.create(context.processingEnv).forEach(::addTypeConverter)
+ BoxedBooleanToBoxedIntConverter.create(context.processingEnv)
+ .forEach(::addTypeConverter)
+ return TypeAdapterStore(context = context, columnTypeAdapters = adapters,
+ typeConverters = converters)
+ }
+ }
+
+ val queryResultBinderProviders = listOf(
+ CursorQueryResultBinderProvider(context),
+ LiveDataQueryResultBinderProvider(context),
+ FlowableQueryResultBinderProvider(context),
+ GuavaListenableFutureQueryResultBinderProvider(context),
+ RxMaybeQueryResultBinderProvider(context),
+ RxSingleQueryResultBinderProvider(context),
+ DataSourceQueryResultBinderProvider(context),
+ DataSourceFactoryQueryResultBinderProvider(context),
+ InstantQueryResultBinderProvider(context)
+ )
+
+ // type mirrors that be converted into columns w/o an extra converter
+ private val knownColumnTypeMirrors by lazy {
+ columnTypeAdapters.map { it.out }
+ }
+
+ /**
+ * Searches 1 way to bind a value into a statement.
+ */
+ fun findStatementValueBinder(
+ input: TypeMirror,
+ affinity: SQLTypeAffinity?
+ ): StatementValueBinder? {
+ if (input.kind == TypeKind.ERROR) {
+ return null
+ }
+ val adapter = findDirectAdapterFor(input, affinity)
+ if (adapter != null) {
+ return adapter
+ }
+ val targetTypes = targetTypeMirrorsFor(affinity)
+ val binder = findTypeConverter(input, targetTypes) ?: return null
+ // columnAdapter should not be null but we are receiving errors on crash in `first()` so
+ // this safeguard allows us to dispatch the real problem to the user (e.g. why we couldn't
+ // find the right adapter)
+ val columnAdapter = getAllColumnAdapters(binder.to).firstOrNull() ?: return null
+ return CompositeAdapter(input, columnAdapter, binder, null)
+ }
+
+ /**
+ * Returns which entities targets the given affinity.
+ */
+ private fun targetTypeMirrorsFor(affinity: SQLTypeAffinity?): List<TypeMirror> {
+ val specifiedTargets = affinity?.getTypeMirrors(context.processingEnv)
+ return if (specifiedTargets == null || specifiedTargets.isEmpty()) {
+ knownColumnTypeMirrors
+ } else {
+ specifiedTargets
+ }
+ }
+
+ /**
+ * Searches 1 way to read it from cursor
+ */
+ fun findCursorValueReader(output: TypeMirror, affinity: SQLTypeAffinity?): CursorValueReader? {
+ if (output.kind == TypeKind.ERROR) {
+ return null
+ }
+ val adapter = findColumnTypeAdapter(output, affinity)
+ if (adapter != null) {
+ // two way is better
+ return adapter
+ }
+ // we could not find a two way version, search for anything
+ val targetTypes = targetTypeMirrorsFor(affinity)
+ val converter = findTypeConverter(targetTypes, output) ?: return null
+ return CompositeAdapter(output,
+ getAllColumnAdapters(converter.from).first(), null, converter)
+ }
+
+ /**
+ * Tries to reverse the converter going through the same nodes, if possible.
+ */
+ @VisibleForTesting
+ fun reverse(converter: TypeConverter): TypeConverter? {
+ return when (converter) {
+ is NoOpConverter -> converter
+ is CompositeTypeConverter -> {
+ val r1 = reverse(converter.conv1) ?: return null
+ val r2 = reverse(converter.conv2) ?: return null
+ CompositeTypeConverter(r2, r1)
+ }
+ else -> {
+ val types = context.processingEnv.typeUtils
+ typeConverters.firstOrNull {
+ types.isSameType(it.from, converter.to) && types
+ .isSameType(it.to, converter.from)
+ }
+ }
+ }
+ }
+
+ /**
+ * Finds a two way converter, if you need 1 way, use findStatementValueBinder or
+ * findCursorValueReader.
+ */
+ fun findColumnTypeAdapter(out: TypeMirror, affinity: SQLTypeAffinity?): ColumnTypeAdapter? {
+ if (out.kind == TypeKind.ERROR) {
+ return null
+ }
+ val adapter = findDirectAdapterFor(out, affinity)
+ if (adapter != null) {
+ return adapter
+ }
+ val targetTypes = targetTypeMirrorsFor(affinity)
+ val intoStatement = findTypeConverter(out, targetTypes) ?: return null
+ // ok found a converter, try the reverse now
+ val fromCursor = reverse(intoStatement) ?: findTypeConverter(intoStatement.to, out)
+ ?: return null
+ return CompositeAdapter(out, getAllColumnAdapters(intoStatement.to).first(), intoStatement,
+ fromCursor)
+ }
+
+ private fun findDirectAdapterFor(
+ out: TypeMirror, affinity: SQLTypeAffinity?): ColumnTypeAdapter? {
+ val adapter = getAllColumnAdapters(out).firstOrNull {
+ affinity == null || it.typeAffinity == affinity
+ }
+ return adapter
+ }
+
+ fun findTypeConverter(input: TypeMirror, output: TypeMirror): TypeConverter? {
+ return findTypeConverter(listOf(input), listOf(output))
+ }
+
+ fun findQueryResultBinder(typeMirror: TypeMirror, query: ParsedQuery): QueryResultBinder {
+ return if (typeMirror.kind == TypeKind.DECLARED) {
+ val declared = MoreTypes.asDeclared(typeMirror)
+ return queryResultBinderProviders.first {
+ it.matches(declared)
+ }.provide(declared, query)
+ } else {
+ InstantQueryResultBinder(findQueryResultAdapter(typeMirror, query))
+ }
+ }
+
+ fun findQueryResultAdapter(typeMirror: TypeMirror, query: ParsedQuery): QueryResultAdapter? {
+ if (typeMirror.kind == TypeKind.ERROR) {
+ return null
+ }
+ if (typeMirror.kind == TypeKind.DECLARED) {
+ val declared = MoreTypes.asDeclared(typeMirror)
+
+ if (declared.typeArguments.isEmpty()) {
+ val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
+ return SingleEntityQueryResultAdapter(rowAdapter)
+ } else if (
+ context.processingEnv.typeUtils.erasure(typeMirror).typeName() ==
+ GuavaBaseTypeNames.OPTIONAL) {
+ // Handle Guava Optional by unpacking its generic type argument and adapting that.
+ // The Optional adapter will reappend the Optional type.
+ val typeArg = declared.typeArguments.first()
+ val rowAdapter = findRowAdapter(typeArg, query) ?: return null
+ return GuavaOptionalQueryResultAdapter(SingleEntityQueryResultAdapter(rowAdapter))
+ } else if (
+ context.processingEnv.typeUtils.erasure(typeMirror).typeName() ==
+ CommonTypeNames.OPTIONAL) {
+ // Handle java.util.Optional similarly.
+ val typeArg = declared.typeArguments.first()
+ val rowAdapter = findRowAdapter(typeArg, query) ?: return null
+ return OptionalQueryResultAdapter(SingleEntityQueryResultAdapter(rowAdapter))
+ } else if (MoreTypes.isTypeOf(java.util.List::class.java, typeMirror)) {
+ val typeArg = declared.typeArguments.first()
+ val rowAdapter = findRowAdapter(typeArg, query) ?: return null
+ return ListQueryResultAdapter(rowAdapter)
+ }
+ return null
+ } else if (typeMirror is ArrayType && typeMirror.componentType.kind != TypeKind.BYTE) {
+ val rowAdapter =
+ findRowAdapter(typeMirror.componentType, query) ?: return null
+ return ArrayQueryResultAdapter(rowAdapter)
+ } else {
+ val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
+ return SingleEntityQueryResultAdapter(rowAdapter)
+ }
+ }
+
+ /**
+ * Find a converter from cursor to the given type mirror.
+ * If there is information about the query result, we try to use it to accept *any* POJO.
+ */
+ @VisibleForTesting
+ fun findRowAdapter(typeMirror: TypeMirror, query: ParsedQuery): RowAdapter? {
+ if (typeMirror.kind == TypeKind.ERROR) {
+ return null
+ }
+ if (typeMirror.kind == TypeKind.DECLARED) {
+ val declared = MoreTypes.asDeclared(typeMirror)
+ if (declared.typeArguments.isNotEmpty()) {
+ // TODO one day support this
+ return null
+ }
+ val resultInfo = query.resultInfo
+
+ val (rowAdapter, rowAdapterLogs) = if (resultInfo != null && query.errors.isEmpty()
+ && resultInfo.error == null) {
+ // if result info is not null, first try a pojo row adapter
+ context.collectLogs { subContext ->
+ val pojo = PojoProcessor(
+ baseContext = subContext,
+ element = MoreTypes.asTypeElement(typeMirror),
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = null
+ ).process()
+ PojoRowAdapter(
+ context = subContext,
+ info = resultInfo,
+ pojo = pojo,
+ out = typeMirror)
+ }
+ } else {
+ Pair(null, null)
+ }
+
+ if (rowAdapter == null && query.resultInfo == null) {
+ // we don't know what query returns. Check for entity.
+ val asElement = MoreTypes.asElement(typeMirror)
+ if (asElement.hasAnnotation(Entity::class)) {
+ return EntityRowAdapter(EntityProcessor(context,
+ MoreElements.asType(asElement)).process())
+ }
+ }
+
+ if (rowAdapter != null && rowAdapterLogs?.hasErrors() != true) {
+ rowAdapterLogs?.writeTo(context.processingEnv)
+ return rowAdapter
+ }
+
+ if ((resultInfo?.columns?.size ?: 1) == 1) {
+ val singleColumn = findCursorValueReader(typeMirror,
+ resultInfo?.columns?.get(0)?.type)
+ if (singleColumn != null) {
+ return SingleColumnRowAdapter(singleColumn)
+ }
+ }
+ // if we tried, return its errors
+ if (rowAdapter != null) {
+ rowAdapterLogs?.writeTo(context.processingEnv)
+ return rowAdapter
+ }
+ if (query.runtimeQueryPlaceholder) {
+ // just go w/ pojo and hope for the best. this happens for @RawQuery where we
+ // try to guess user's intention and hope that their query fits the result.
+ val pojo = PojoProcessor(
+ baseContext = context,
+ element = MoreTypes.asTypeElement(typeMirror),
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = null
+ ).process()
+ return PojoRowAdapter(
+ context = context,
+ info = null,
+ pojo = pojo,
+ out = typeMirror)
+ }
+ return null
+ } else {
+ val singleColumn = findCursorValueReader(typeMirror, null) ?: return null
+ return SingleColumnRowAdapter(singleColumn)
+ }
+ }
+
+ fun findQueryParameterAdapter(typeMirror: TypeMirror): QueryParameterAdapter? {
+ if (MoreTypes.isType(typeMirror)
+ && (MoreTypes.isTypeOf(java.util.List::class.java, typeMirror)
+ || MoreTypes.isTypeOf(java.util.Set::class.java, typeMirror))) {
+ val declared = MoreTypes.asDeclared(typeMirror)
+ val binder = findStatementValueBinder(declared.typeArguments.first(),
+ null)
+ if (binder != null) {
+ return CollectionQueryParameterAdapter(binder)
+ } else {
+ // maybe user wants to convert this collection themselves. look for a match
+ val collectionBinder = findStatementValueBinder(typeMirror, null) ?: return null
+ return BasicQueryParameterAdapter(collectionBinder)
+ }
+ } else if (typeMirror is ArrayType && typeMirror.componentType.kind != TypeKind.BYTE) {
+ val component = typeMirror.componentType
+ val binder = findStatementValueBinder(component, null) ?: return null
+ return ArrayQueryParameterAdapter(binder)
+ } else {
+ val binder = findStatementValueBinder(typeMirror, null) ?: return null
+ return BasicQueryParameterAdapter(binder)
+ }
+ }
+
+ private fun findTypeConverter(input: TypeMirror, outputs: List<TypeMirror>): TypeConverter? {
+ return findTypeConverter(listOf(input), outputs)
+ }
+
+ private fun findTypeConverter(input: List<TypeMirror>, output: TypeMirror): TypeConverter? {
+ return findTypeConverter(input, listOf(output))
+ }
+
+ private fun findTypeConverter(
+ inputs: List<TypeMirror>, outputs: List<TypeMirror>): TypeConverter? {
+ if (inputs.isEmpty()) {
+ return null
+ }
+ val types = context.processingEnv.typeUtils
+ inputs.forEach { input ->
+ if (outputs.any { output -> types.isSameType(input, output) }) {
+ return NoOpConverter(input)
+ }
+ }
+
+ val excludes = arrayListOf<TypeMirror>()
+
+ val queue = LinkedList<TypeConverter>()
+ fun exactMatch(candidates: List<TypeConverter>): TypeConverter? {
+ return candidates.firstOrNull {
+ outputs.any { output -> types.isSameType(output, it.to) }
+ }
+ }
+ inputs.forEach { input ->
+ val candidates = getAllTypeConverters(input, excludes)
+ val match = exactMatch(candidates)
+ if (match != null) {
+ return match
+ }
+ candidates.forEach {
+ excludes.add(it.to)
+ queue.add(it)
+ }
+ }
+ excludes.addAll(inputs)
+ while (queue.isNotEmpty()) {
+ val prev = queue.pop()
+ val from = prev.to
+ val candidates = getAllTypeConverters(from, excludes)
+ val match = exactMatch(candidates)
+ if (match != null) {
+ return CompositeTypeConverter(prev, match)
+ }
+ candidates.forEach {
+ excludes.add(it.to)
+ queue.add(CompositeTypeConverter(prev, it))
+ }
+ }
+ return null
+ }
+
+ private fun getAllColumnAdapters(input: TypeMirror): List<ColumnTypeAdapter> {
+ return columnTypeAdapters.filter {
+ context.processingEnv.typeUtils.isSameType(input, it.out)
+ }
+ }
+
+ /**
+ * Returns all type converters that can receive input type and return into another type.
+ * The returned list is ordered by priority such that if we have an exact match, it is
+ * prioritized.
+ */
+ private fun getAllTypeConverters(input: TypeMirror, excludes: List<TypeMirror>):
+ List<TypeConverter> {
+ val types = context.processingEnv.typeUtils
+ // for input, check assignability because it defines whether we can use the method or not.
+ // for excludes, use exact match
+ return typeConverters.filter { converter ->
+ types.isAssignable(input, converter.from) &&
+ !excludes.any { types.isSameType(it, converter.to) }
+ }.sortedByDescending {
+ // if it is the same, prioritize
+ if (types.isSameType(it.from, input)) {
+ 2
+ } else {
+ 1
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/CursorQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/CursorQueryResultBinderProvider.kt
new file mode 100644
index 0000000..1afe889
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/CursorQueryResultBinderProvider.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.solver.QueryResultBinderProvider
+import androidx.room.solver.query.result.CursorQueryResultBinder
+import androidx.room.solver.query.result.QueryResultBinder
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.DeclaredType
+
+class CursorQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
+ override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ return CursorQueryResultBinder()
+ }
+
+ override fun matches(declared: DeclaredType): Boolean =
+ declared.typeArguments.size == 0 && TypeName.get(declared) == AndroidTypeNames.CURSOR
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
new file mode 100644
index 0000000..be41c6f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
@@ -0,0 +1,63 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.PagingTypeNames
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.QueryResultBinderProvider
+import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder
+import androidx.room.solver.query.result.ListQueryResultAdapter
+import androidx.room.solver.query.result.PositionalDataSourceQueryResultBinder
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+
+class DataSourceFactoryQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
+ private val dataSourceFactoryTypeMirror: TypeMirror? by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY.toString())?.asType()
+ }
+
+ override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ if (query.tables.isEmpty()) {
+ context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+ val typeArg = declared.typeArguments[1]
+ val adapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
+ ListQueryResultAdapter(it)
+ }
+
+ val tableNames = ((adapter?.accessedTableNames() ?: emptyList())
+ + query.tables.map { it.name }).toSet()
+ val countedBinder = PositionalDataSourceQueryResultBinder(adapter, tableNames)
+ return DataSourceFactoryQueryResultBinder(countedBinder)
+ }
+
+ override fun matches(declared: DeclaredType): Boolean =
+ declared.typeArguments.size == 2 && isLivePagedList(declared)
+
+ private fun isLivePagedList(declared: DeclaredType): Boolean {
+ if (dataSourceFactoryTypeMirror == null) {
+ return false
+ }
+ val erasure = context.processingEnv.typeUtils.erasure(declared)
+ // we don't want to return paged list unless explicitly requested
+ return context.processingEnv.typeUtils.isAssignable(dataSourceFactoryTypeMirror, erasure)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
new file mode 100644
index 0000000..09dbf7c
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
@@ -0,0 +1,74 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.PagingTypeNames
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.QueryResultBinderProvider
+import androidx.room.solver.query.result.ListQueryResultAdapter
+import androidx.room.solver.query.result.PositionalDataSourceQueryResultBinder
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+
+class DataSourceQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
+ private val dataSourceTypeMirror: TypeMirror? by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(PagingTypeNames.DATA_SOURCE.toString())?.asType()
+ }
+
+ private val positionalDataSourceTypeMirror: TypeMirror? by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(PagingTypeNames.POSITIONAL_DATA_SOURCE.toString())?.asType()
+ }
+
+ override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ if (query.tables.isEmpty()) {
+ context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+ val typeArg = declared.typeArguments.last()
+ val listAdapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
+ ListQueryResultAdapter(it)
+ }
+ val tableNames = ((listAdapter?.accessedTableNames() ?: emptyList())
+ + query.tables.map { it.name }).toSet()
+ return PositionalDataSourceQueryResultBinder(listAdapter, tableNames)
+ }
+
+ override fun matches(declared: DeclaredType): Boolean {
+ if (dataSourceTypeMirror == null || positionalDataSourceTypeMirror == null) {
+ return false
+ }
+ if (declared.typeArguments.isEmpty()) {
+ return false
+ }
+ val erasure = context.processingEnv.typeUtils.erasure(declared)
+ val isDataSource = context.processingEnv.typeUtils
+ .isAssignable(erasure, dataSourceTypeMirror)
+ if (!isDataSource) {
+ return false
+ }
+ val isPositional = context.processingEnv.typeUtils
+ .isAssignable(erasure, positionalDataSourceTypeMirror)
+ if (!isPositional) {
+ context.logger.e(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
+ }
+ return true
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/FlowableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/FlowableQueryResultBinderProvider.kt
new file mode 100644
index 0000000..4acd1b8
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/FlowableQueryResultBinderProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.RoomRxJava2TypeNames
+import androidx.room.ext.RxJava2TypeNames
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.ObservableQueryResultBinderProvider
+import androidx.room.solver.query.result.FlowableQueryResultBinder
+import androidx.room.solver.query.result.QueryResultAdapter
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+
+class FlowableQueryResultBinderProvider(context: Context) :
+ ObservableQueryResultBinderProvider(context) {
+ private val flowableTypeMirror: TypeMirror? by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(RxJava2TypeNames.FLOWABLE.toString())?.asType()
+ }
+
+ private val hasRxJava2Artifact by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(RoomRxJava2TypeNames.RX_ROOM.toString()) != null
+ }
+
+ override fun extractTypeArg(declared: DeclaredType): TypeMirror = declared.typeArguments.first()
+
+ override fun create(typeArg: TypeMirror, resultAdapter: QueryResultAdapter?,
+ tableNames: Set<String>): QueryResultBinder {
+ return FlowableQueryResultBinder(
+ typeArg = typeArg,
+ queryTableNames = tableNames,
+ adapter = resultAdapter)
+ }
+
+ override fun matches(declared: DeclaredType): Boolean =
+ declared.typeArguments.size == 1 && isRxJava2Publisher(declared)
+
+ private fun isRxJava2Publisher(declared: DeclaredType): Boolean {
+ if (flowableTypeMirror == null) {
+ return false
+ }
+ val erasure = context.processingEnv.typeUtils.erasure(declared)
+ val match = context.processingEnv.typeUtils.isAssignable(flowableTypeMirror, erasure)
+ if (match && !hasRxJava2Artifact) {
+ context.logger.e(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+ }
+ return match
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/GuavaListenableFutureQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/GuavaListenableFutureQueryResultBinderProvider.kt
new file mode 100644
index 0000000..16db69b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/GuavaListenableFutureQueryResultBinderProvider.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.RoomGuavaTypeNames
+import androidx.room.ext.typeName
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.QueryResultBinderProvider
+import androidx.room.solver.query.result.GuavaListenableFutureQueryResultBinder
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+
+class GuavaListenableFutureQueryResultBinderProvider(val context: Context)
+ : QueryResultBinderProvider {
+
+ private val hasGuavaRoom by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(RoomGuavaTypeNames.GUAVA_ROOM.toString()) != null
+ }
+
+ /**
+ * Returns the {@link GuavaListenableFutureQueryResultBinder} instance for the input type, if
+ * possible.
+ *
+ * <p>Emits a compiler error if the Guava Room extension library is not linked.
+ */
+ override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ if (!hasGuavaRoom) {
+ context.logger.e(ProcessorErrors.MISSING_ROOM_GUAVA_ARTIFACT)
+ }
+
+ // Use the type T inside ListenableFuture<T> as the type to adapt and to pass into
+ // the binder.
+ val adapter = context.typeAdapterStore.findQueryResultAdapter(
+ declared.typeArguments.first(), query)
+ return GuavaListenableFutureQueryResultBinder(
+ declared.typeArguments.first(), adapter)
+ }
+
+ /**
+ * Returns true iff the input {@code declared} type is ListenableFuture<T>.
+ */
+ override fun matches(declared: DeclaredType): Boolean =
+ declared.typeArguments.size == 1 &&
+ context.processingEnv.typeUtils.erasure(declared).typeName() ==
+ GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/InstantQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/InstantQueryResultBinderProvider.kt
new file mode 100644
index 0000000..d0b1660
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/InstantQueryResultBinderProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.solver.QueryResultBinderProvider
+import androidx.room.solver.query.result.InstantQueryResultBinder
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+
+class InstantQueryResultBinderProvider(val context: Context) : QueryResultBinderProvider {
+ override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ return InstantQueryResultBinder(
+ context.typeAdapterStore.findQueryResultAdapter(declared, query))
+ }
+
+ override fun matches(declared: DeclaredType): Boolean = true
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/LiveDataQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/LiveDataQueryResultBinderProvider.kt
new file mode 100644
index 0000000..1aa2da9
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/LiveDataQueryResultBinderProvider.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.processor.Context
+import androidx.room.solver.ObservableQueryResultBinderProvider
+import androidx.room.solver.query.result.LiveDataQueryResultBinder
+import androidx.room.solver.query.result.QueryResultAdapter
+import androidx.room.solver.query.result.QueryResultBinder
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeMirror
+
+class LiveDataQueryResultBinderProvider(context: Context)
+ : ObservableQueryResultBinderProvider(context) {
+ private val liveDataTypeMirror: TypeMirror? by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString())?.asType()
+ }
+
+ override fun extractTypeArg(declared: DeclaredType): TypeMirror = declared.typeArguments.first()
+
+ override fun create(typeArg: TypeMirror, resultAdapter: QueryResultAdapter?,
+ tableNames: Set<String>): QueryResultBinder {
+ return LiveDataQueryResultBinder(
+ typeArg = typeArg,
+ tableNames = tableNames,
+ adapter = resultAdapter)
+ }
+
+ override fun matches(declared: DeclaredType): Boolean =
+ declared.typeArguments.size == 1 && isLiveData(declared)
+
+ private fun isLiveData(declared: DeclaredType): Boolean {
+ if (liveDataTypeMirror == null) {
+ return false
+ }
+ val erasure = context.processingEnv.typeUtils.erasure(declared)
+ return context.processingEnv.typeUtils.isAssignable(liveDataTypeMirror, erasure)
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
new file mode 100644
index 0000000..6ac7d91
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
@@ -0,0 +1,60 @@
+/*
+ * 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 androidx.room.solver.binderprovider
+
+import androidx.room.ext.RoomRxJava2TypeNames
+import androidx.room.ext.typeName
+import androidx.room.parser.ParsedQuery
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.QueryResultBinderProvider
+import androidx.room.solver.query.result.QueryResultBinder
+import androidx.room.solver.query.result.RxCallableQueryResultBinder
+import javax.lang.model.type.DeclaredType
+
+sealed class RxCallableQueryResultBinderProvider(val context: Context,
+ val rxType: RxCallableQueryResultBinder.RxType)
+ : QueryResultBinderProvider {
+ private val hasRxJava2Artifact by lazy {
+ context.processingEnv.elementUtils
+ .getTypeElement(RoomRxJava2TypeNames.RX_ROOM.toString()) != null
+ }
+
+ override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+ val typeArg = declared.typeArguments.first()
+ val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
+ return RxCallableQueryResultBinder(rxType, typeArg, adapter)
+ }
+
+ override fun matches(declared: DeclaredType): Boolean =
+ declared.typeArguments.size == 1 && matchesRxType(declared)
+
+ private fun matchesRxType(declared: DeclaredType): Boolean {
+ val erasure = context.processingEnv.typeUtils.erasure(declared)
+ val match = erasure.typeName() == rxType.className
+ if (match && !hasRxJava2Artifact) {
+ context.logger.e(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+ }
+ return match
+ }
+}
+
+class RxSingleQueryResultBinderProvider(context: Context)
+ : RxCallableQueryResultBinderProvider(context, RxCallableQueryResultBinder.RxType.SINGLE)
+
+class RxMaybeQueryResultBinderProvider(context: Context)
+ : RxCallableQueryResultBinderProvider(context, RxCallableQueryResultBinder.RxType.MAYBE)
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/ArrayQueryParameterAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/ArrayQueryParameterAdapter.kt
new file mode 100644
index 0000000..ea0c788
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/ArrayQueryParameterAdapter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.parameter
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.types.StatementValueBinder
+import com.squareup.javapoet.TypeName
+
+/**
+ * Binds ARRAY(T) (e.g. int[]) into String[] args of a query.
+ */
+class ArrayQueryParameterAdapter(val bindAdapter: StatementValueBinder)
+ : QueryParameterAdapter(true) {
+ override fun bindToStmt(inputVarName: String, stmtVarName: String, startIndexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ val itrVar = scope.getTmpVar("_item")
+ beginControlFlow("for ($T $L : $L)", bindAdapter.typeMirror().typeName(), itrVar,
+ inputVarName).apply {
+ bindAdapter.bindToStmt(stmtVarName, startIndexVarName, itrVar, scope)
+ addStatement("$L ++", startIndexVarName)
+ }
+ endControlFlow()
+ }
+ }
+
+ override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("final $T $L = $L.length", TypeName.INT, outputVarName, inputVarName)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/BasicQueryParameterAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/BasicQueryParameterAdapter.kt
new file mode 100644
index 0000000..0ab9bee
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/BasicQueryParameterAdapter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.parameter
+
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.types.StatementValueBinder
+
+/**
+ * Knows how to convert a query parameter into arguments
+ */
+class BasicQueryParameterAdapter(val bindAdapter: StatementValueBinder)
+ : QueryParameterAdapter(false) {
+ override fun bindToStmt(inputVarName: String, stmtVarName: String, startIndexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ bindAdapter.bindToStmt(stmtVarName, startIndexVarName, inputVarName, scope)
+ }
+ }
+
+ override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+ throw UnsupportedOperationException("should not call getArgCount on basic adapters." +
+ "It is always one.")
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/CollectionQueryParameterAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/CollectionQueryParameterAdapter.kt
new file mode 100644
index 0000000..9d3a231
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/CollectionQueryParameterAdapter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.parameter
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.types.StatementValueBinder
+import com.squareup.javapoet.TypeName
+
+/**
+ * Binds Collection<T> (e.g. List<T>) into String[] query args.
+ */
+class CollectionQueryParameterAdapter(val bindAdapter: StatementValueBinder)
+ : QueryParameterAdapter(true) {
+ override fun bindToStmt(inputVarName: String, stmtVarName: String, startIndexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ val itrVar = scope.getTmpVar("_item")
+ beginControlFlow("for ($T $L : $L)", bindAdapter.typeMirror().typeName(), itrVar,
+ inputVarName).apply {
+ bindAdapter.bindToStmt(stmtVarName, startIndexVarName, itrVar, scope)
+ addStatement("$L ++", startIndexVarName)
+ }
+ endControlFlow()
+ }
+ }
+
+ override fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("final $T $L = $L.size()", TypeName.INT, outputVarName, inputVarName)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/QueryParameterAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/QueryParameterAdapter.kt
new file mode 100644
index 0000000..3c29ad9
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/parameter/QueryParameterAdapter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.parameter
+
+import androidx.room.solver.CodeGenScope
+
+/**
+ * Knows how to convert a query parameter into query arguments.
+ */
+abstract class QueryParameterAdapter(val isMultiple: Boolean) {
+ /**
+ * Must bind the value into the statement at the given index.
+ */
+ abstract fun bindToStmt(
+ inputVarName: String,
+ stmtVarName: String,
+ startIndexVarName: String,
+ scope: CodeGenScope)
+
+ /**
+ * Should declare and set the given value with the count
+ */
+ abstract fun getArgCount(inputVarName: String, outputVarName: String, scope: CodeGenScope)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ArrayQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ArrayQueryResultAdapter.kt
new file mode 100644
index 0000000..86a1c41
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ArrayQueryResultAdapter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.TypeName
+
+class ArrayQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
+ val type = rowAdapter.out
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ rowAdapter?.onCursorReady(cursorVarName, scope)
+
+ val arrayType = ArrayTypeName.of(type.typeName())
+ addStatement("final $T $L = new $T[$L.getCount()]",
+ arrayType, outVarName, type.typeName(), cursorVarName)
+ val tmpVarName = scope.getTmpVar("_item")
+ val indexVar = scope.getTmpVar("_index")
+ addStatement("$T $L = 0", TypeName.INT, indexVar)
+ beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
+ addStatement("final $T $L", type.typeName(), tmpVarName)
+ rowAdapter?.convert(tmpVarName, cursorVarName, scope)
+ addStatement("$L[$L] = $L", outVarName, indexVar, tmpVarName)
+ addStatement("$L ++", indexVar)
+ }
+ endControlFlow()
+ rowAdapter?.onCursorFinished()?.invoke(scope)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/BaseObservableQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/BaseObservableQueryResultBinder.kt
new file mode 100644
index 0000000..679a684
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/BaseObservableQueryResultBinder.kt
@@ -0,0 +1,74 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import javax.lang.model.element.Modifier
+
+/**
+ * Base class for query result binders that observe the database. It includes common functionality
+ * like creating a finalizer to release the query or creating the actual adapter call code.
+ */
+abstract class BaseObservableQueryResultBinder(adapter: QueryResultAdapter?)
+ : QueryResultBinder(adapter) {
+
+ protected fun createFinalizeMethod(roomSQLiteQueryVar: String): MethodSpec {
+ return MethodSpec.methodBuilder("finalize").apply {
+ addModifiers(Modifier.PROTECTED)
+ addAnnotation(Override::class.java)
+ addStatement("$L.release()", roomSQLiteQueryVar)
+ }.build()
+ }
+
+ protected fun createRunQueryAndReturnStatements(builder: MethodSpec.Builder,
+ roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ val transactionWrapper = if (inTransaction) {
+ builder.transactionWrapper(dbField)
+ } else {
+ null
+ }
+ val outVar = scope.getTmpVar("_result")
+ val cursorVar = scope.getTmpVar("_cursor")
+ transactionWrapper?.beginTransactionWithControlFlow()
+ builder.apply {
+ addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
+ DaoWriter.dbField, roomSQLiteQueryVar)
+ beginControlFlow("try").apply {
+ val adapterScope = scope.fork()
+ adapter?.convert(outVar, cursorVar, adapterScope)
+ addCode(adapterScope.builder().build())
+ transactionWrapper?.commitTransaction()
+ addStatement("return $L", outVar)
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$L.close()", cursorVar)
+ }
+ endControlFlow()
+ }
+ transactionWrapper?.endTransactionWithControlFlow()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt
new file mode 100644
index 0000000..282e4f5
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CursorQueryResultBinder.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.FieldSpec
+
+class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ val builder = scope.builder()
+ val transactionWrapper = if (inTransaction) {
+ builder.transactionWrapper(dbField)
+ } else {
+ null
+ }
+ transactionWrapper?.beginTransactionWithControlFlow()
+ val resultName = scope.getTmpVar("_tmpResult")
+ builder.addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, resultName,
+ dbField, roomSQLiteQueryVar)
+ transactionWrapper?.commitTransaction()
+ builder.addStatement("return $L", resultName)
+ transactionWrapper?.endTransactionWithControlFlow()
+ }
+
+ companion object {
+ private val NO_OP_RESULT_ADAPTER = object : QueryResultAdapter(null) {
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
new file mode 100644
index 0000000..0f1ecc1
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/DataSourceFactoryQueryResultBinder.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+
+class DataSourceFactoryQueryResultBinder(
+ val positionalDataSourceQueryResultBinder: PositionalDataSourceQueryResultBinder)
+ : QueryResultBinder(positionalDataSourceQueryResultBinder.listAdapter) {
+ @Suppress("HasPlatformType")
+ val typeName = positionalDataSourceQueryResultBinder.itemTypeName
+ override fun convertAndReturn(
+ roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ scope.builder().apply {
+ val pagedListProvider = TypeSpec
+ .anonymousClassBuilder("").apply {
+ superclass(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY,
+ Integer::class.typeName(), typeName))
+ addMethod(createCreateMethod(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope))
+ }.build()
+ addStatement("return $L", pagedListProvider)
+ }
+ }
+
+ private fun createCreateMethod(
+ roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ): MethodSpec = MethodSpec.methodBuilder("create").apply {
+ addAnnotation(Override::class.java)
+ addModifiers(Modifier.PUBLIC)
+ returns(positionalDataSourceQueryResultBinder.typeName)
+ val countedBinderScope = scope.fork()
+ positionalDataSourceQueryResultBinder.convertAndReturn(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = true,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = countedBinderScope)
+ addCode(countedBinderScope.builder().build())
+ }.build()
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
new file mode 100644
index 0000000..a66f9e1
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Entity
+import androidx.room.writer.EntityCursorConverterWriter
+import com.squareup.javapoet.MethodSpec
+
+class EntityRowAdapter(val entity: Entity) : RowAdapter(entity.type) {
+ lateinit var methodSpec: MethodSpec
+ override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
+ methodSpec = scope.writer.getOrCreateMethod(EntityCursorConverterWriter(entity))
+ }
+
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("$L = $N($L)", outVarName, methodSpec, cursorVarName)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/FlowableQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/FlowableQueryResultBinder.kt
new file mode 100644
index 0000000..08225fc
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/FlowableQueryResultBinder.kt
@@ -0,0 +1,71 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomRxJava2TypeNames
+import androidx.room.ext.T
+import androidx.room.ext.arrayTypeName
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Binds the result as an RxJava2 Flowable.
+ */
+class FlowableQueryResultBinder(val typeArg: TypeMirror, val queryTableNames: Set<String>,
+ adapter: QueryResultAdapter?)
+ : BaseObservableQueryResultBinder(adapter) {
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ val callableImpl = TypeSpec.anonymousClassBuilder("").apply {
+ val typeName = typeArg.typeName()
+ superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
+ typeName))
+ addMethod(MethodSpec.methodBuilder("call").apply {
+ returns(typeName)
+ addException(Exception::class.typeName())
+ addModifiers(Modifier.PUBLIC)
+ addAnnotation(Override::class.java)
+ createRunQueryAndReturnStatements(builder = this,
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ inTransaction = inTransaction,
+ dbField = dbField,
+ scope = scope)
+ }.build())
+ if (canReleaseQuery) {
+ addMethod(createFinalizeMethod(roomSQLiteQueryVar))
+ }
+ }.build()
+ scope.builder().apply {
+ val tableNamesList = queryTableNames.joinToString(",") { "\"$it\"" }
+ addStatement("return $T.createFlowable($N, new $T{$L}, $L)",
+ RoomRxJava2TypeNames.RX_ROOM, DaoWriter.dbField,
+ String::class.arrayTypeName(), tableNamesList, callableImpl)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
new file mode 100644
index 0000000..f0cdb4f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2018 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 androidx.room.solver.query.result
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomGuavaTypeNames
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A ResultBinder that emits a ListenableFuture<T> where T is the input {@code typeArg}.
+ *
+ * <p>The Future runs on the background thread Executor.
+ */
+class GuavaListenableFutureQueryResultBinder(
+ val typeArg: TypeMirror,
+ adapter: QueryResultAdapter?)
+ : BaseObservableQueryResultBinder(adapter) {
+
+ override fun convertAndReturn(
+ roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ // Callable<T>
+ val callableImpl = createCallableOfT(
+ roomSQLiteQueryVar,
+ dbField,
+ inTransaction,
+ scope)
+
+ scope.builder().apply {
+ addStatement(
+ "return $T.createListenableFuture($L, $L, $L)",
+ RoomGuavaTypeNames.GUAVA_ROOM,
+ callableImpl,
+ roomSQLiteQueryVar,
+ canReleaseQuery)
+ }
+ }
+
+ /**
+ * Returns an anonymous subclass of Callable<T> that executes the database transaction and
+ * constitutes the result T.
+ *
+ * <p>Note that this method does not release the query object.
+ */
+ private fun createCallableOfT(
+ roomSQLiteQueryVar: String,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope): TypeSpec {
+ return TypeSpec.anonymousClassBuilder("").apply {
+ superclass(
+ ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
+ typeArg.typeName()))
+ addMethod(
+ MethodSpec.methodBuilder("call").apply {
+ // public T call() throws Exception {}
+ returns(typeArg.typeName())
+ addAnnotation(Override::class.typeName())
+ addModifiers(Modifier.PUBLIC)
+ addException(Exception::class.typeName())
+
+ // Body.
+ val transactionWrapper = if (inTransaction) {
+ transactionWrapper(dbField)
+ } else {
+ null
+ }
+ transactionWrapper?.beginTransactionWithControlFlow()
+ apply {
+ val outVar = scope.getTmpVar("_result")
+ val cursorVar = scope.getTmpVar("_cursor")
+ addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR,
+ cursorVar,
+ DaoWriter.dbField, roomSQLiteQueryVar)
+ beginControlFlow("try").apply {
+ val adapterScope = scope.fork()
+ adapter?.convert(outVar, cursorVar, adapterScope)
+ addCode(adapterScope.builder().build())
+ transactionWrapper?.commitTransaction()
+ addStatement("return $L", outVar)
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$L.close()", cursorVar)
+ }
+ endControlFlow()
+ }
+ transactionWrapper?.endTransactionWithControlFlow()
+ }.build())
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
new file mode 100644
index 0000000..efdc8b8
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 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 androidx.room.solver.query.result
+
+import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ParameterizedTypeName
+
+/**
+ * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
+ * {@link com.google.common.base.Optional}.
+ */
+class GuavaOptionalQueryResultAdapter(private val resultAdapter: SingleEntityQueryResultAdapter)
+ : QueryResultAdapter(resultAdapter.rowAdapter) {
+ val type = resultAdapter.rowAdapter?.out
+ override fun convert(
+ outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ val valueVarName = scope.getTmpVar("_value")
+ resultAdapter?.convert(valueVarName, cursorVarName, scope)
+ addStatement(
+ "final $T $L = $T.fromNullable($L)",
+ ParameterizedTypeName.get(GuavaBaseTypeNames.OPTIONAL, type?.typeName()),
+ outVarName,
+ GuavaBaseTypeNames.OPTIONAL,
+ valueVarName)
+ }
+ }
+}
+
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
new file mode 100644
index 0000000..406f41e
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
@@ -0,0 +1,61 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+
+/**
+ * Instantly runs and returns the query.
+ */
+class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ val transactionWrapper = if (inTransaction) {
+ scope.builder().transactionWrapper(dbField)
+ } else {
+ null
+ }
+ transactionWrapper?.beginTransactionWithControlFlow()
+ scope.builder().apply {
+ val outVar = scope.getTmpVar("_result")
+ val cursorVar = scope.getTmpVar("_cursor")
+ addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
+ DaoWriter.dbField, roomSQLiteQueryVar)
+ beginControlFlow("try").apply {
+ adapter?.convert(outVar, cursorVar, scope)
+ transactionWrapper?.commitTransaction()
+ addStatement("return $L", outVar)
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$L.close()", cursorVar)
+ if (canReleaseQuery) {
+ addStatement("$L.release()", roomSQLiteQueryVar)
+ }
+ }
+ endControlFlow()
+ }
+ transactionWrapper?.endTransactionWithControlFlow()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
new file mode 100644
index 0000000..a38c431
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import java.util.ArrayList
+
+class ListQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
+ val type = rowAdapter.out
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ rowAdapter?.onCursorReady(cursorVarName, scope)
+ val collectionType = ParameterizedTypeName
+ .get(ClassName.get(List::class.java), type.typeName())
+ val arrayListType = ParameterizedTypeName
+ .get(ClassName.get(ArrayList::class.java), type.typeName())
+ addStatement("final $T $L = new $T($L.getCount())",
+ collectionType, outVarName, arrayListType, cursorVarName)
+ val tmpVarName = scope.getTmpVar("_item")
+ beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
+ addStatement("final $T $L", type.typeName(), tmpVarName)
+ rowAdapter?.convert(tmpVarName, cursorVarName, scope)
+ addStatement("$L.add($L)", outVarName, tmpVarName)
+ }
+ endControlFlow()
+ rowAdapter?.onCursorFinished()?.invoke(scope)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
new file mode 100644
index 0000000..0f2e1c3
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -0,0 +1,119 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.annotation.NonNull
+import androidx.room.ext.L
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.RoomTypeNames.INVALIDATION_OBSERVER
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Converts the query into a LiveData and returns it. No query is run until necessary.
+ */
+class LiveDataQueryResultBinder(val typeArg: TypeMirror, val tableNames: Set<String>,
+ adapter: QueryResultAdapter?)
+ : BaseObservableQueryResultBinder(adapter) {
+ @Suppress("JoinDeclarationAndAssignment")
+ override fun convertAndReturn(
+ roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ) {
+ val typeName = typeArg.typeName()
+
+ val liveDataImpl = TypeSpec.anonymousClassBuilder("").apply {
+ superclass(ParameterizedTypeName.get(LifecyclesTypeNames.COMPUTABLE_LIVE_DATA,
+ typeName))
+ val observerField = FieldSpec.builder(RoomTypeNames.INVALIDATION_OBSERVER,
+ scope.getTmpVar("_observer"), Modifier.PRIVATE).build()
+ addField(observerField)
+ addMethod(createComputeMethod(
+ observerField = observerField,
+ typeName = typeName,
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope
+ ))
+ if (canReleaseQuery) {
+ addMethod(createFinalizeMethod(roomSQLiteQueryVar))
+ }
+ }.build()
+ scope.builder().apply {
+ addStatement("return $L.getLiveData()", liveDataImpl)
+ }
+ }
+
+ private fun createComputeMethod(
+ roomSQLiteQueryVar: String,
+ typeName: TypeName,
+ observerField: FieldSpec,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope
+ ): MethodSpec {
+ return MethodSpec.methodBuilder("compute").apply {
+ addAnnotation(Override::class.java)
+ addModifiers(Modifier.PROTECTED)
+ returns(typeName)
+
+ beginControlFlow("if ($N == null)", observerField).apply {
+ addStatement("$N = $L", observerField, createAnonymousObserver())
+ addStatement("$N.getInvalidationTracker().addWeakObserver($N)",
+ dbField, observerField)
+ }
+ endControlFlow()
+
+ createRunQueryAndReturnStatements(builder = this,
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope)
+ }.build()
+ }
+
+ private fun createAnonymousObserver(): TypeSpec {
+ val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
+ return TypeSpec.anonymousClassBuilder(tableNamesList).apply {
+ superclass(INVALIDATION_OBSERVER)
+ addMethod(MethodSpec.methodBuilder("onInvalidated").apply {
+ returns(TypeName.VOID)
+ addAnnotation(Override::class.java)
+ addParameter(ParameterSpec.builder(
+ ParameterizedTypeName.get(Set::class.java, String::class.java), "tables")
+ .addAnnotation(NonNull::class.java)
+ .build())
+ addModifiers(Modifier.PUBLIC)
+ addStatement("invalidate()")
+ }.build())
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
new file mode 100644
index 0000000..2c23867
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 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 androidx.room.solver.query.result
+
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ParameterizedTypeName
+
+/**
+ * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
+ * {@link java.util.Optional}.
+ *
+ * <p>n.b. this will only be useful if the project uses Java 8.
+ */
+class OptionalQueryResultAdapter(private val resultAdapter: SingleEntityQueryResultAdapter)
+ : QueryResultAdapter(resultAdapter.rowAdapter) {
+ val type = resultAdapter.rowAdapter?.out
+ override fun convert(
+ outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ val valueVarName = scope.getTmpVar("_value")
+ resultAdapter?.convert(valueVarName, cursorVarName, scope)
+ addStatement(
+ "final $T $L = $T.ofNullable($L)",
+ ParameterizedTypeName.get(CommonTypeNames.OPTIONAL, type?.typeName()),
+ outVarName,
+ CommonTypeNames.OPTIONAL,
+ valueVarName)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
new file mode 100644
index 0000000..a8fe00f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -0,0 +1,157 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.CodeGenScope
+import androidx.room.verifier.QueryResultInfo
+import androidx.room.vo.Field
+import androidx.room.vo.FieldWithIndex
+import androidx.room.vo.Pojo
+import androidx.room.vo.RelationCollector
+import androidx.room.vo.Warning
+import androidx.room.writer.FieldReadWriteWriter
+import com.squareup.javapoet.TypeName
+import stripNonJava
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Creates the entity from the given info.
+ * <p>
+ * The info comes from the query processor so we know about the order of columns in the result etc.
+ */
+class PojoRowAdapter(
+ context: Context, private val info: QueryResultInfo?,
+ val pojo: Pojo, out: TypeMirror) : RowAdapter(out) {
+ val mapping: Mapping
+ val relationCollectors: List<RelationCollector>
+
+ init {
+ // toMutableList documentation is not clear if it copies so lets be safe.
+ val remainingFields = pojo.fields.mapTo(mutableListOf(), { it })
+ val unusedColumns = arrayListOf<String>()
+ val matchedFields: List<Field>
+ if (info != null) {
+ matchedFields = info.columns.map { column ->
+ // first check remaining, otherwise check any. maybe developer wants to map the same
+ // column into 2 fields. (if they want to post process etc)
+ val field = remainingFields.firstOrNull { it.columnName == column.name } ?:
+ pojo.fields.firstOrNull { it.columnName == column.name }
+ if (field == null) {
+ unusedColumns.add(column.name)
+ null
+ } else {
+ remainingFields.remove(field)
+ field
+ }
+ }.filterNotNull()
+ if (unusedColumns.isNotEmpty() || remainingFields.isNotEmpty()) {
+ val warningMsg = ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = pojo.typeName,
+ unusedColumns = unusedColumns,
+ allColumns = info.columns.map { it.name },
+ unusedFields = remainingFields,
+ allFields = pojo.fields
+ )
+ context.logger.w(Warning.CURSOR_MISMATCH, null, warningMsg)
+ }
+ val nonNulls = remainingFields.filter { it.nonNull }
+ if (nonNulls.isNotEmpty()) {
+ context.logger.e(ProcessorErrors.pojoMissingNonNull(
+ pojoTypeName = pojo.typeName,
+ missingPojoFields = nonNulls.map { it.name },
+ allQueryColumns = info.columns.map { it.name }))
+ }
+ if (matchedFields.isEmpty()) {
+ context.logger.e(ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ }
+ } else {
+ matchedFields = remainingFields.map { it }
+ remainingFields.clear()
+ }
+ relationCollectors = RelationCollector.createCollectors(context, pojo.relations)
+
+ mapping = Mapping(
+ matchedFields = matchedFields,
+ unusedColumns = unusedColumns,
+ unusedFields = remainingFields
+ )
+ }
+
+ fun relationTableNames(): List<String> {
+ return relationCollectors.flatMap {
+ val queryTableNames = it.loadAllQuery.tables.map { it.name }
+ if (it.rowAdapter is PojoRowAdapter) {
+ it.rowAdapter.relationTableNames() + queryTableNames
+ } else {
+ queryTableNames
+ }
+ }.distinct()
+ }
+
+ override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
+ relationCollectors.forEach { it.writeInitCode(scope) }
+ mapping.fieldsWithIndices = mapping.matchedFields.map {
+ val indexVar = scope.getTmpVar("_cursorIndexOf${it.name.stripNonJava().capitalize()}")
+ val indexMethod = if (info == null) {
+ "getColumnIndex"
+ } else {
+ "getColumnIndexOrThrow"
+ }
+ scope.builder().addStatement("final $T $L = $L.$L($S)",
+ TypeName.INT, indexVar, cursorVarName, indexMethod, it.columnName)
+ FieldWithIndex(field = it, indexVar = indexVar, alwaysExists = info != null)
+ }
+ }
+
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ FieldReadWriteWriter.readFromCursor(
+ outVar = outVarName,
+ outPojo = pojo,
+ cursorVar = cursorVarName,
+ fieldsWithIndices = mapping.fieldsWithIndices,
+ relationCollectors = relationCollectors,
+ scope = scope)
+ }
+ }
+
+ override fun onCursorFinished(): ((CodeGenScope) -> Unit)? =
+ if (relationCollectors.isEmpty()) {
+ // it is important to return empty to notify that we don't need any post process
+ // task
+ null
+ } else {
+ { scope ->
+ relationCollectors.forEach { collector ->
+ collector.writeCollectionCode(scope)
+ }
+ }
+ }
+
+ data class Mapping(
+ val matchedFields: List<Field>,
+ val unusedColumns: List<String>,
+ val unusedFields: List<Field>) {
+ // set when cursor is ready.
+ lateinit var fieldsWithIndices: List<FieldWithIndex>
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
new file mode 100644
index 0000000..96db613
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
@@ -0,0 +1,73 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+
+class PositionalDataSourceQueryResultBinder(
+ val listAdapter: ListQueryResultAdapter?,
+ val tableNames: Set<String>) : QueryResultBinder(listAdapter) {
+ val itemTypeName: TypeName = listAdapter?.rowAdapter?.out?.typeName() ?: TypeName.OBJECT
+ val typeName: ParameterizedTypeName = ParameterizedTypeName.get(
+ RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE, itemTypeName)
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ // first comma for table names comes from the string since it might be empty in which case
+ // we don't need a comma. If list is empty, this prevents generating bad code (it is still
+ // an error to have empty list but that is already reported while item is processed)
+ val tableNamesList = tableNames.joinToString("") { ", \"$it\"" }
+ val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L $L",
+ dbField, roomSQLiteQueryVar, inTransaction, tableNamesList).apply {
+ superclass(typeName)
+ addMethod(createConvertRowsMethod(scope))
+ }.build()
+ scope.builder().apply {
+ addStatement("return $L", spec)
+ }
+ }
+
+ private fun createConvertRowsMethod(scope: CodeGenScope): MethodSpec =
+ MethodSpec.methodBuilder("convertRows").apply {
+ addAnnotation(Override::class.java)
+ addModifiers(Modifier.PROTECTED)
+ returns(ParameterizedTypeName.get(CommonTypeNames.LIST, itemTypeName))
+ val cursorParam = ParameterSpec.builder(AndroidTypeNames.CURSOR, "cursor")
+ .build()
+ addParameter(cursorParam)
+ val resultVar = scope.getTmpVar("_res")
+ val rowsScope = scope.fork()
+ listAdapter?.convert(resultVar, cursorParam.name, rowsScope)
+ addCode(rowsScope.builder().build())
+ addStatement("return $L", resultVar)
+ }.build()
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
new file mode 100644
index 0000000..a3be347
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.solver.CodeGenScope
+
+/**
+ * Gets a Cursor and converts it into the return type of a method annotated with @Query.
+ */
+abstract class QueryResultAdapter(val rowAdapter: RowAdapter?) {
+ abstract fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope)
+ fun accessedTableNames(): List<String> {
+ return (rowAdapter as? PojoRowAdapter)?.relationTableNames() ?: emptyList<String>()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
new file mode 100644
index 0000000..71d8139
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.FieldSpec
+
+/**
+ * Connects the query, db and the ResultAdapter.
+ * <p>
+ * The default implementation is InstantResultBinder. If the query is deferred rather than executed
+ * directly, such alternative implementations can be implement using this interface (e.g. LiveData,
+ * Rx, caching etc)
+ */
+abstract class QueryResultBinder(val adapter: QueryResultAdapter?) {
+ /**
+ * receives the sql, bind args and adapter and generates the code that runs the query
+ * and returns the result.
+ */
+ abstract fun convertAndReturn(
+ roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean, // false if query is provided by the user
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
new file mode 100644
index 0000000..a9d4db6
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.solver.CodeGenScope
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Converts a row of a cursor result into an Entity or a primitive.
+ * <p>
+ * An instance of this is created for each usage so that it can keep local variables.
+ */
+abstract class RowAdapter(val out: TypeMirror) {
+ /**
+ * Called when cursor variable is ready, good place to put initialization code.
+ */
+ open fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {}
+
+ /**
+ * Called to convert a single row.
+ */
+ abstract fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope)
+
+ /**
+ * Called when the cursor is finished. It is important to return null if no operation is
+ * necessary so that caller can understand that we can do lazy loading.
+ */
+ open fun onCursorFinished(): ((scope: CodeGenScope) -> Unit)? = null
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxCallableQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxCallableQueryResultBinder.kt
new file mode 100644
index 0000000..b94d0c7
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxCallableQueryResultBinder.kt
@@ -0,0 +1,115 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomRxJava2TypeNames
+import androidx.room.ext.RxJava2TypeNames
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Generic Result binder for Rx classes that accept a callable.
+ */
+class RxCallableQueryResultBinder(val rxType: RxType,
+ val typeArg: TypeMirror,
+ adapter: QueryResultAdapter?)
+ : QueryResultBinder(adapter) {
+ override fun convertAndReturn(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope) {
+ val callable = TypeSpec.anonymousClassBuilder("").apply {
+ val typeName = typeArg.typeName()
+ superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
+ typeName))
+ addMethod(createCallMethod(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = canReleaseQuery,
+ dbField = dbField,
+ inTransaction = inTransaction,
+ scope = scope))
+ }.build()
+ scope.builder().apply {
+ addStatement("return $T.fromCallable($L)", rxType.className, callable)
+ }
+ }
+
+ private fun createCallMethod(roomSQLiteQueryVar: String,
+ canReleaseQuery: Boolean,
+ dbField: FieldSpec,
+ inTransaction: Boolean,
+ scope: CodeGenScope): MethodSpec {
+ val adapterScope = scope.fork()
+ return MethodSpec.methodBuilder("call").apply {
+ returns(typeArg.typeName())
+ addException(Exception::class.typeName())
+ addModifiers(Modifier.PUBLIC)
+ addAnnotation(Override::class.java)
+ val transactionWrapper = if (inTransaction) {
+ transactionWrapper(dbField)
+ } else {
+ null
+ }
+ transactionWrapper?.beginTransactionWithControlFlow()
+ val outVar = scope.getTmpVar("_result")
+ val cursorVar = scope.getTmpVar("_cursor")
+ addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
+ dbField, roomSQLiteQueryVar)
+ beginControlFlow("try").apply {
+ adapter?.convert(outVar, cursorVar, adapterScope)
+ addCode(adapterScope.generate())
+ if (!rxType.canBeNull) {
+ beginControlFlow("if($L == null)", outVar).apply {
+ addStatement("throw new $T($S + $L.getSql())",
+ RoomRxJava2TypeNames.RX_EMPTY_RESULT_SET_EXCEPTION,
+ "Query returned empty result set: ",
+ roomSQLiteQueryVar)
+ }
+ endControlFlow()
+ }
+ transactionWrapper?.commitTransaction()
+ addStatement("return $L", outVar)
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$L.close()", cursorVar)
+ if (canReleaseQuery) {
+ addStatement("$L.release()", roomSQLiteQueryVar)
+ }
+ }
+ endControlFlow()
+ transactionWrapper?.endTransactionWithControlFlow()
+ }.build()
+ }
+
+ enum class RxType(val className: ClassName, val canBeNull: Boolean) {
+ SINGLE(RxJava2TypeNames.SINGLE, canBeNull = false),
+ MAYBE(RxJava2TypeNames.MAYBE, canBeNull = true);
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/SingleColumnRowAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/SingleColumnRowAdapter.kt
new file mode 100644
index 0000000..d06b47a
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/SingleColumnRowAdapter.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.types.CursorValueReader
+
+/**
+ * Wraps a row adapter when there is only 1 item with 1 column in the response.
+ */
+class SingleColumnRowAdapter(val reader: CursorValueReader) : RowAdapter(reader.typeMirror()) {
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ reader.readFromCursor(outVarName, cursorVarName, "0", scope)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/SingleEntityQueryResultAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/SingleEntityQueryResultAdapter.kt
new file mode 100644
index 0000000..c6eef30
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/SingleEntityQueryResultAdapter.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query.result
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import defaultValue
+
+/**
+ * Wraps a row adapter when there is only 1 item in the result
+ */
+class SingleEntityQueryResultAdapter(rowAdapter: RowAdapter) : QueryResultAdapter(rowAdapter) {
+ val type = rowAdapter.out
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ rowAdapter?.onCursorReady(cursorVarName, scope)
+ addStatement("final $T $L", type.typeName(), outVarName)
+ beginControlFlow("if($L.moveToFirst())", cursorVarName)
+ rowAdapter?.convert(outVarName, cursorVarName, scope)
+ nextControlFlow("else").apply {
+ addStatement("$L = $L", outVarName, rowAdapter?.out?.defaultValue())
+ }
+ endControlFlow()
+ rowAdapter?.onCursorFinished()?.invoke(scope)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/TransactionWrapper.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/TransactionWrapper.kt
new file mode 100644
index 0000000..373035b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/TransactionWrapper.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 androidx.room.solver.query.result
+
+import androidx.room.ext.N
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+
+/**
+ * helper class to create correct transaction code.
+ */
+interface TransactionWrapper {
+ fun beginTransactionWithControlFlow()
+ fun commitTransaction()
+ fun endTransactionWithControlFlow()
+}
+
+fun MethodSpec.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
+ override fun beginTransactionWithControlFlow() {
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try")
+ }
+
+ override fun commitTransaction() {
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ }
+
+ override fun endTransactionWithControlFlow() {
+ nextControlFlow("finally")
+ addStatement("$N.endTransaction()", dbField)
+ endControlFlow()
+ }
+}
+
+fun CodeBlock.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
+ override fun beginTransactionWithControlFlow() {
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try")
+ }
+
+ override fun commitTransaction() {
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ }
+
+ override fun endTransactionWithControlFlow() {
+ nextControlFlow("finally")
+ addStatement("$N.endTransaction()", dbField)
+ endControlFlow()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
new file mode 100644
index 0000000..a648eac
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.solver.CodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+
+/**
+ * int to boolean adapter.
+ */
+object BoxedBooleanToBoxedIntConverter {
+ fun create(processingEnvironment: ProcessingEnvironment): List<TypeConverter> {
+ val tBoolean = processingEnvironment.elementUtils.getTypeElement("java.lang.Boolean")
+ .asType()
+ val tInt = processingEnvironment.elementUtils.getTypeElement("java.lang.Integer")
+ .asType()
+ return listOf(
+ object : TypeConverter(tBoolean, tInt) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().addStatement("$L = $L == null ? null : ($L ? 1 : 0)",
+ outputVarName, inputVarName, inputVarName)
+ }
+ },
+ object : TypeConverter(tInt, tBoolean) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().addStatement("$L = $L == null ? null : $L != 0",
+ outputVarName, inputVarName, inputVarName)
+ }
+ }
+ )
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
new file mode 100644
index 0000000..732260b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.solver.CodeGenScope
+import com.google.auto.common.MoreTypes
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Adapters for all boxed primitives that has direct cursor mappings.
+ */
+open class BoxedPrimitiveColumnTypeAdapter(
+ boxed: TypeMirror,
+ val primitiveAdapter: PrimitiveColumnTypeAdapter
+) : ColumnTypeAdapter(boxed, primitiveAdapter.typeAffinity) {
+ companion object {
+ fun createBoxedPrimitiveAdapters(
+ processingEnvironment: ProcessingEnvironment,
+ primitiveAdapters: List<PrimitiveColumnTypeAdapter>
+ ): List<ColumnTypeAdapter> {
+
+ return primitiveAdapters.map {
+ BoxedPrimitiveColumnTypeAdapter(
+ processingEnvironment.typeUtils
+ .boxedClass(MoreTypes.asPrimitiveType(it.out)).asType(),
+ it
+ )
+ }
+ }
+ }
+
+ override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ beginControlFlow("if ($L == null)", valueVarName).apply {
+ addStatement("$L.bindNull($L)", stmtName, indexVarName)
+ }
+ nextControlFlow("else").apply {
+ primitiveAdapter.bindToStmt(stmtName, indexVarName, valueVarName, scope)
+ }
+ endControlFlow()
+ }
+ }
+
+ override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ beginControlFlow("if ($L.isNull($L))", cursorVarName, indexVarName).apply {
+ addStatement("$L = null", outVarName)
+ }
+ nextControlFlow("else").apply {
+ primitiveAdapter.readFromCursor(outVarName, cursorVarName, indexVarName, scope)
+ }
+ endControlFlow()
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/ByteArrayColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/ByteArrayColumnTypeAdapter.kt
new file mode 100644
index 0000000..4a7b230
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/ByteArrayColumnTypeAdapter.kt
@@ -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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.solver.CodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+
+class ByteArrayColumnTypeAdapter(env: ProcessingEnvironment) : ColumnTypeAdapter(
+ out = env.typeUtils.getArrayType(env.typeUtils.getPrimitiveType(TypeKind.BYTE)),
+ typeAffinity = SQLTypeAffinity.BLOB) {
+ override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("$L = $L.getBlob($L)", outVarName, cursorVarName, indexVarName)
+ }
+
+ override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ beginControlFlow("if ($L == null)", valueVarName)
+ .addStatement("$L.bindNull($L)", stmtName, indexVarName)
+ nextControlFlow("else")
+ .addStatement("$L.bindBlob($L, $L)", stmtName, indexVarName, valueVarName)
+ endControlFlow()
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/ColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/ColumnTypeAdapter.kt
new file mode 100644
index 0000000..e8833bf
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/ColumnTypeAdapter.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.parser.SQLTypeAffinity
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A code generator that can read a field from Cursor and write a field to a Statement
+ */
+abstract class ColumnTypeAdapter(val out: TypeMirror, val typeAffinity: SQLTypeAffinity) :
+ StatementValueBinder, CursorValueReader {
+ val outTypeName: TypeName by lazy { TypeName.get(out) }
+ override fun typeMirror() = out
+ override fun affinity(): SQLTypeAffinity = typeAffinity
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/CompositeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/CompositeAdapter.kt
new file mode 100644
index 0000000..c285f79
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/CompositeAdapter.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A column adapter that uses a type converter to do the conversion. The type converter may be
+ * a composite one.
+ */
+class CompositeAdapter(out: TypeMirror, val columnTypeAdapter: ColumnTypeAdapter,
+ val intoStatementConverter: TypeConverter?,
+ val fromCursorConverter: TypeConverter?)
+ : ColumnTypeAdapter(out, columnTypeAdapter.typeAffinity) {
+ override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
+ scope: CodeGenScope) {
+ if (fromCursorConverter == null) {
+ return
+ }
+ scope.builder().apply {
+ val tmpCursorValue = scope.getTmpVar()
+ addStatement("final $T $L", columnTypeAdapter.outTypeName, tmpCursorValue)
+ columnTypeAdapter.readFromCursor(tmpCursorValue, cursorVarName, indexVarName, scope)
+ fromCursorConverter.convert(tmpCursorValue, outVarName, scope)
+ }
+ }
+
+ override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
+ scope: CodeGenScope) {
+ if (intoStatementConverter == null) {
+ return
+ }
+ scope.builder().apply {
+ val tmpVar = scope.getTmpVar()
+ addStatement("final $T $L", columnTypeAdapter.out, tmpVar)
+ intoStatementConverter.convert(valueVarName, tmpVar, scope)
+ columnTypeAdapter.bindToStmt(stmtName, indexVarName, tmpVar, scope)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
new file mode 100644
index 0000000..e233b50
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+
+/**
+ * combines 2 type converters
+ */
+class CompositeTypeConverter(val conv1: TypeConverter, val conv2: TypeConverter) : TypeConverter(
+ conv1.from, conv2.to) {
+ override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ val tmp = scope.getTmpVar()
+ addStatement("final $T $L", conv1.to.typeName(), tmp)
+ conv1.convert(inputVarName, tmp, scope)
+ conv2.convert(tmp, outputVarName, scope)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/CursorValueReader.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/CursorValueReader.kt
new file mode 100644
index 0000000..30d6b6f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/CursorValueReader.kt
@@ -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 androidx.room.solver.types
+
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.solver.CodeGenScope
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Reads value from a cursor at the given index.
+ * see: StatementValueBinder
+ */
+interface CursorValueReader {
+ fun affinity(): SQLTypeAffinity
+ fun typeMirror(): TypeMirror
+ fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
+ scope: CodeGenScope)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
new file mode 100644
index 0000000..5d174c0
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.CustomTypeConverter
+import androidx.room.writer.ClassWriter
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import javax.lang.model.element.Modifier
+
+/**
+ * Wraps a type converter specified by the developer and forwards calls to it.
+ */
+class CustomTypeConverterWrapper(val custom: CustomTypeConverter)
+ : TypeConverter(custom.from, custom.to) {
+
+ override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ if (custom.isStatic) {
+ addStatement("$L = $T.$L($L)",
+ outputVarName, custom.typeName,
+ custom.methodName, inputVarName)
+ } else {
+ addStatement("$L = $N.$L($L)",
+ outputVarName, typeConverter(scope),
+ custom.methodName, inputVarName)
+ }
+ }
+ }
+
+ fun typeConverter(scope: CodeGenScope): FieldSpec {
+ val baseName = (custom.typeName as ClassName).simpleName().decapitalize()
+ return scope.writer.getOrCreateField(object : ClassWriter.SharedFieldSpec(
+ baseName, custom.typeName) {
+ override fun getUniqueKey(): String {
+ return "converter_${custom.typeName}"
+ }
+
+ override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
+ builder.addModifiers(Modifier.PRIVATE)
+ builder.addModifiers(Modifier.FINAL)
+ builder.initializer("new $T()", custom.typeName)
+ }
+ })
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
new file mode 100644
index 0000000..352843c
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.solver.CodeGenScope
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Yes, we need this when user input is the same as the desired output.
+ * <p>
+ * Each query parameter receives an adapter that converts it into a String (or String[]). This
+ * TypeAdapter basically serves as a wrapper for converting String parameter into the String[] of
+ * the query. Not having this would require us to special case handle String, String[], List<String>
+ * etc.
+ */
+class NoOpConverter(type: TypeMirror) : TypeConverter(
+ type, type) {
+ override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("$L = $L", outputVarName, inputVarName)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveBooleanToIntConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveBooleanToIntConverter.kt
new file mode 100644
index 0000000..b8f63d9
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveBooleanToIntConverter.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.solver.CodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind.BOOLEAN
+import javax.lang.model.type.TypeKind.INT
+
+/**
+ * int to boolean adapter.
+ */
+object PrimitiveBooleanToIntConverter {
+ fun create(processingEnvironment: ProcessingEnvironment): List<TypeConverter> {
+ val tBoolean = processingEnvironment.typeUtils.getPrimitiveType(BOOLEAN)
+ val tInt = processingEnvironment.typeUtils.getPrimitiveType(INT)
+ return listOf(
+ object : TypeConverter(tBoolean, tInt) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().addStatement("$L = $L ? 1 : 0", outputVarName, inputVarName)
+ }
+ },
+ object : TypeConverter(tInt, tBoolean) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().addStatement("$L = $L != 0", outputVarName, inputVarName)
+ }
+ })
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
new file mode 100644
index 0000000..8ff759c
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.ext.typeName
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.parser.SQLTypeAffinity.REAL
+import androidx.room.solver.CodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.PrimitiveType
+import javax.lang.model.type.TypeKind.BYTE
+import javax.lang.model.type.TypeKind.CHAR
+import javax.lang.model.type.TypeKind.DOUBLE
+import javax.lang.model.type.TypeKind.FLOAT
+import javax.lang.model.type.TypeKind.INT
+import javax.lang.model.type.TypeKind.LONG
+import javax.lang.model.type.TypeKind.SHORT
+
+/**
+ * Adapters for all primitives that has direct cursor mappings.
+ */
+open class PrimitiveColumnTypeAdapter(out: PrimitiveType,
+ val cursorGetter: String,
+ val stmtSetter: String,
+ typeAffinity: SQLTypeAffinity)
+ : ColumnTypeAdapter(out, typeAffinity) {
+ val cast = if (cursorGetter == "get${out.typeName().toString().capitalize()}")
+ ""
+ else
+ "(${out.typeName()}) "
+
+ companion object {
+ fun createPrimitiveAdapters(
+ processingEnvironment: ProcessingEnvironment
+ ): List<PrimitiveColumnTypeAdapter> {
+ return listOf(
+ Triple(INT, "getInt", "bindLong"),
+ Triple(SHORT, "getShort", "bindLong"),
+ Triple(BYTE, "getShort", "bindLong"),
+ Triple(LONG, "getLong", "bindLong"),
+ Triple(CHAR, "getInt", "bindLong"),
+ Triple(FLOAT, "getFloat", "bindDouble"),
+ Triple(DOUBLE, "getDouble", "bindDouble")
+ ).map {
+ PrimitiveColumnTypeAdapter(
+ out = processingEnvironment.typeUtils.getPrimitiveType(it.first),
+ cursorGetter = it.second,
+ stmtSetter = it.third,
+ typeAffinity = when (it.first) {
+ INT, SHORT, BYTE, LONG, CHAR -> SQLTypeAffinity.INTEGER
+ FLOAT, DOUBLE -> REAL
+ else -> throw IllegalArgumentException("invalid type")
+ }
+ )
+ }
+ }
+ }
+
+ override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
+ scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("$L.$L($L, $L)", stmtName, stmtSetter, indexVarName, valueVarName)
+ }
+
+ override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("$L = $L$L.$L($L)", outVarName, cast, cursorVarName,
+ cursorGetter, indexVarName)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/StatementValueBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/StatementValueBinder.kt
new file mode 100644
index 0000000..995ca98
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/StatementValueBinder.kt
@@ -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 androidx.room.solver.types
+
+import androidx.room.solver.CodeGenScope
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Binds a value into a statement
+ * see: CursorValueReader
+ */
+interface StatementValueBinder {
+ fun typeMirror(): TypeMirror
+ fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
+ scope: CodeGenScope)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
new file mode 100644
index 0000000..5b15699
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.ext.L
+import androidx.room.parser.SQLTypeAffinity.TEXT
+import androidx.room.solver.CodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+
+class StringColumnTypeAdapter(processingEnvironment: ProcessingEnvironment)
+ : ColumnTypeAdapter((processingEnvironment.elementUtils.getTypeElement(
+ String::class.java.canonicalName)).asType(), TEXT) {
+ override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
+ scope: CodeGenScope) {
+ scope.builder()
+ .addStatement("$L = $L.getString($L)", outVarName, cursorVarName, indexVarName)
+ }
+
+ override fun bindToStmt(stmtName: String, indexVarName: String, valueVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ beginControlFlow("if ($L == null)", valueVarName)
+ .addStatement("$L.bindNull($L)", stmtName, indexVarName)
+ nextControlFlow("else")
+ .addStatement("$L.bindString($L, $L)", stmtName, indexVarName, valueVarName)
+ endControlFlow()
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
new file mode 100644
index 0000000..f967135
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.types
+
+import androidx.room.solver.CodeGenScope
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A code generator that can convert from 1 type to another
+ */
+abstract class TypeConverter(val from: TypeMirror, val to: TypeMirror) {
+ abstract fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope)
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/verifier/ColumnInfo.kt b/room/compiler/src/main/kotlin/androidx/room/verifier/ColumnInfo.kt
new file mode 100644
index 0000000..788beaf
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/verifier/ColumnInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 androidx.room.verifier
+
+import androidx.room.parser.SQLTypeAffinity
+
+/**
+ * Represents a column in a query response
+ */
+data class ColumnInfo(val name: String, val type: SQLTypeAffinity)
diff --git a/room/compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerificaitonErrors.kt b/room/compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerificaitonErrors.kt
new file mode 100644
index 0000000..c9b7394
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerificaitonErrors.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.room.verifier
+
+import java.sql.SQLException
+
+object DatabaseVerificaitonErrors {
+ private val CANNOT_CREATE_TABLE: String = "Create table statement had an error: %s"
+ fun cannotCreateTable(exception: SQLException): String {
+ return CANNOT_CREATE_TABLE.format(exception.message)
+ }
+
+ private val CANNOT_VERIFY_QUERY: String = "There is a problem with the query: %s"
+ fun cannotVerifyQuery(exception: SQLException): String {
+ return CANNOT_VERIFY_QUERY.format(exception.message)
+ }
+
+ private val CANNOT_CREATE_SQLITE_CONNECTION: String = "Room cannot create an SQLite" +
+ " connection to verify the queries. Query verification will be disabled. Error: %s"
+ fun cannotCreateConnection(exception: Exception): String {
+ return CANNOT_CREATE_SQLITE_CONNECTION.format(exception.message)
+ }
+
+ val CANNOT_GET_TMP_JAVA_DIR = "Cannot read tmp java dir which is necessary to load sqlite" +
+ " lib. Database SQL verification will be disabled"
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt b/room/compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
new file mode 100644
index 0000000..cadc44b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
@@ -0,0 +1,125 @@
+/*
+ * 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 androidx.room.verifier
+
+import androidx.room.processor.Context
+import androidx.room.vo.Entity
+import androidx.room.vo.Warning
+import columnInfo
+import org.sqlite.JDBC
+import java.io.File
+import java.sql.Connection
+import java.sql.DriverManager
+import java.sql.SQLException
+import java.util.UUID
+import java.util.regex.Pattern
+import javax.lang.model.element.Element
+
+/**
+ * Builds an in-memory version of the database and verifies the queries against it.
+ * This class is also used to resolve the return types.
+ */
+class DatabaseVerifier private constructor(
+ val connection: Connection, val context: Context, val entities: List<Entity>) {
+ companion object {
+ private const val CONNECTION_URL = "jdbc:sqlite::memory:"
+ /**
+ * Taken from:
+ * https://github.com/robolectric/robolectric/blob/master/shadows/framework/
+ * src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java#L94
+ *
+ * This is actually not accurate because it might swap anything since it does not parse
+ * SQL. That being said, for the verification purposes, it does not matter and clearly
+ * much easier than parsing and rebuilding the query.
+ */
+ private val COLLATE_LOCALIZED_UNICODE_PATTERN = Pattern.compile(
+ "\\s+COLLATE\\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE)
+
+ init {
+ // see: https://github.com/xerial/sqlite-jdbc/issues/97
+ val tmpDir = System.getProperty("java.io.tmpdir")
+ if (tmpDir != null) {
+ val outDir = File(tmpDir, "room-${UUID.randomUUID()}")
+ outDir.mkdirs()
+ outDir.deleteOnExit()
+ System.setProperty("org.sqlite.tmpdir", outDir.absolutePath)
+ // dummy call to trigger JDBC initialization so that we can unregister it
+ JDBC.isValidURL(CONNECTION_URL)
+ unregisterDrivers()
+ }
+ }
+
+ /**
+ * Tries to create a verifier but returns null if it cannot find the driver.
+ */
+ fun create(context: Context, element: Element, entities: List<Entity>): DatabaseVerifier? {
+ return try {
+ val connection = JDBC.createConnection(CONNECTION_URL, java.util.Properties())
+ DatabaseVerifier(connection, context, entities)
+ } catch (ex: Exception) {
+ context.logger.w(Warning.CANNOT_CREATE_VERIFICATION_DATABASE, element,
+ DatabaseVerificaitonErrors.cannotCreateConnection(ex))
+ null
+ }
+ }
+
+ /**
+ * Unregisters the JDBC driver. If we don't do this, we'll leak the driver which leaks a
+ * whole class loader.
+ * see: https://github.com/xerial/sqlite-jdbc/issues/267
+ * see: https://issuetracker.google.com/issues/62473121
+ */
+ private fun unregisterDrivers() {
+ try {
+ DriverManager.getDriver(CONNECTION_URL)?.let {
+ DriverManager.deregisterDriver(it)
+ }
+ } catch (t: Throwable) {
+ System.err.println("Room: cannot unregister driver ${t.message}")
+ }
+ }
+ }
+ init {
+ entities.forEach { entity ->
+ val stmt = connection.createStatement()
+ stmt.executeUpdate(stripLocalizeCollations(entity.createTableQuery))
+ }
+ }
+
+ fun analyze(sql: String): QueryResultInfo {
+ return try {
+ val stmt = connection.prepareStatement(stripLocalizeCollations(sql))
+ QueryResultInfo(stmt.columnInfo())
+ } catch (ex: SQLException) {
+ QueryResultInfo(emptyList(), ex)
+ }
+ }
+
+ private fun stripLocalizeCollations(sql: String) =
+ COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql).replaceAll(" COLLATE NOCASE")
+
+ fun closeConnection(context: Context) {
+ if (!connection.isClosed) {
+ try {
+ connection.close()
+ } catch (t: Throwable) {
+ //ignore.
+ context.logger.d("failed to close the database connection ${t.message}")
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/verifier/QueryResultInfo.kt b/room/compiler/src/main/kotlin/androidx/room/verifier/QueryResultInfo.kt
new file mode 100644
index 0000000..a8c9476
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/verifier/QueryResultInfo.kt
@@ -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 androidx.room.verifier
+
+import java.sql.SQLException
+
+/**
+ * Represents the result of a query.
+ * <p>
+ * This information is obtained by preparing the query against an in memory database at compile
+ * time.
+ */
+data class QueryResultInfo(val columns: List<ColumnInfo>, val error: SQLException? = null)
diff --git a/room/compiler/src/main/kotlin/androidx/room/verifier/jdbc_ext.kt b/room/compiler/src/main/kotlin/androidx/room/verifier/jdbc_ext.kt
new file mode 100644
index 0000000..df2ee65
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/verifier/jdbc_ext.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.verifier.ColumnInfo
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+import java.sql.ResultSetMetaData
+import java.sql.SQLException
+
+internal fun <T> ResultSet.collect(f: (ResultSet) -> T): List<T> {
+ val result = arrayListOf<T>()
+ try {
+ while (next()) {
+ result.add(f.invoke(this))
+ }
+ } finally {
+ close()
+ }
+ return result
+}
+
+private fun <T> PreparedStatement.map(f: (Int, ResultSetMetaData) -> T): List<T> {
+ val columnCount = try {
+ metaData.columnCount
+ } catch (ex: SQLException) {
+ // ignore, no-result query
+ 0
+ }
+ // return is separate than data creation because we want to know who throws the exception
+ return (1.rangeTo(columnCount)).map { f(it, metaData) }
+}
+
+internal fun PreparedStatement.columnNames(): List<String> {
+ return map { index, data -> data.getColumnName(index) }
+}
+
+private fun PreparedStatement.tryGetAffinity(columnIndex: Int): SQLTypeAffinity {
+ return try {
+ SQLTypeAffinity.valueOf(metaData.getColumnTypeName(columnIndex).capitalize())
+ } catch (ex: IllegalArgumentException) {
+ SQLTypeAffinity.NULL
+ }
+}
+
+internal fun PreparedStatement.columnInfo(): List<ColumnInfo> {
+ //see: http://sqlite.1065341.n5.nabble.com/Column-order-in-resultset-td23127.html
+ return map { index, data -> ColumnInfo(data.getColumnName(index), tryGetAffinity(index)) }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/CallType.kt b/room/compiler/src/main/kotlin/androidx/room/vo/CallType.kt
new file mode 100644
index 0000000..e81a41b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/CallType.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+enum class CallType {
+ FIELD,
+ METHOD,
+ CONSTRUCTOR
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Constructor.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Constructor.kt
new file mode 100644
index 0000000..fdab508
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Constructor.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 androidx.room.vo
+
+import javax.lang.model.element.ExecutableElement
+
+/**
+ * For each Entity / Pojo we process has a constructor. It might be the empty constructor or a
+ * constructor with fields.
+ */
+data class Constructor(val element: ExecutableElement, val params: List<Param>) {
+
+ fun hasField(field: Field): Boolean {
+ return params.any {
+ when (it) {
+ is FieldParam -> it.field === field
+ is EmbeddedParam -> it.embedded.field === field
+ else -> false
+ }
+ }
+ }
+
+ class FieldParam(val field: Field) : Param(ParamType.FIELD) {
+ override fun log(): String = field.getPath()
+ }
+
+ class EmbeddedParam(val embedded: EmbeddedField) : Param(ParamType.EMBEDDED) {
+ override fun log(): String = embedded.field.getPath()
+ }
+
+ abstract class Param(val type: ParamType) {
+ abstract fun log(): String
+ }
+
+ enum class ParamType {
+ FIELD,
+ EMBEDDED
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt b/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt
new file mode 100644
index 0000000..e1f16d7
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.ext.hasAnyOf
+import androidx.room.ext.typeName
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Generated when we parse a method annotated with TypeConverter.
+ */
+data class CustomTypeConverter(val type: TypeMirror,
+ val method: ExecutableElement,
+ val from: TypeMirror, val to: TypeMirror) {
+ val typeName: TypeName by lazy { type.typeName() }
+ val fromTypeName: TypeName by lazy { from.typeName() }
+ val toTypeName: TypeName by lazy { to.typeName() }
+ val methodName by lazy { method.simpleName.toString() }
+ val isStatic by lazy { method.hasAnyOf(Modifier.STATIC) }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt
new file mode 100644
index 0000000..e12fa0e
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+
+data class Dao(
+ val element: TypeElement, val type: DeclaredType,
+ val queryMethods: List<QueryMethod>,
+ val rawQueryMethods: List<RawQueryMethod>,
+ val insertionMethods: List<InsertionMethod>,
+ val deletionMethods: List<DeletionMethod>,
+ val updateMethods: List<UpdateMethod>,
+ val transactionMethods: List<TransactionMethod>,
+ val constructorParamType: TypeName?) {
+ // parsed dao might have a suffix if it is used in multiple databases.
+ private var suffix: String? = null
+
+ fun setSuffix(newSuffix: String) {
+ if (this.suffix != null) {
+ throw IllegalStateException("cannot set suffix twice")
+ }
+ this.suffix = if (newSuffix == "") "" else "_$newSuffix"
+ }
+
+ val typeName: ClassName by lazy { ClassName.get(element) }
+
+ val shortcutMethods: List<ShortcutMethod> by lazy {
+ deletionMethods + updateMethods
+ }
+
+ private val implClassName by lazy {
+ if (suffix == null) {
+ suffix = ""
+ }
+ val path = arrayListOf<String>()
+ var enclosing = element.enclosingElement
+ while (enclosing is TypeElement) {
+ path.add(ClassName.get(enclosing as TypeElement).simpleName())
+ enclosing = enclosing.enclosingElement
+ }
+ path.reversed().joinToString("_") + "${typeName.simpleName()}${suffix}_Impl"
+ }
+
+ val implTypeName: ClassName by lazy {
+ ClassName.get(typeName.packageName(), implClassName)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/DaoMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/DaoMethod.kt
new file mode 100644
index 0000000..9266294
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/DaoMethod.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import javax.lang.model.element.Element
+
+/**
+ * References a method that returns a dao in a Database
+ */
+data class DaoMethod(val element: Element, val name: String, val dao: Dao)
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt
new file mode 100644
index 0000000..e79a901
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.RoomMasterTable
+import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.SchemaBundle
+import com.squareup.javapoet.ClassName
+import org.apache.commons.codec.digest.DigestUtils
+import java.io.File
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Holds information about a class annotated with Database.
+ */
+data class Database(val element: TypeElement,
+ val type: TypeMirror,
+ val entities: List<Entity>,
+ val daoMethods: List<DaoMethod>,
+ val version: Int,
+ val exportSchema: Boolean,
+ val enableForeignKeys: Boolean) {
+ val typeName: ClassName by lazy { ClassName.get(element) }
+
+ private val implClassName by lazy {
+ "${typeName.simpleNames().joinToString("_")}_Impl"
+ }
+
+ val implTypeName: ClassName by lazy {
+ ClassName.get(typeName.packageName(), implClassName)
+ }
+
+ val bundle by lazy {
+ DatabaseBundle(version, identityHash, entities.map(Entity::toBundle),
+ listOf(RoomMasterTable.CREATE_QUERY,
+ RoomMasterTable.createInsertQuery(identityHash)))
+ }
+
+ /**
+ * Create a has that identifies this database definition so that at runtime we can check to
+ * ensure developer didn't forget to update the version.
+ */
+ val identityHash: String by lazy {
+ val idKey = SchemaIdentityKey()
+ idKey.appendSorted(entities)
+ idKey.hash()
+ }
+
+ val legacyIdentityHash: String by lazy {
+ val entityDescriptions = entities
+ .sortedBy { it.tableName }
+ .map { it.createTableQuery }
+ val indexDescriptions = entities
+ .flatMap { entity ->
+ entity.indices.map { index ->
+ index.createQuery(entity.tableName)
+ }
+ }
+ val input = (entityDescriptions + indexDescriptions).joinToString("¯\\_(ツ)_/¯")
+ DigestUtils.md5Hex(input)
+ }
+
+ fun exportSchema(file: File) {
+ val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
+ if (file.exists()) {
+ val existing = file.inputStream().use {
+ SchemaBundle.deserialize(it)
+ }
+ if (existing.isSchemaEqual(schemaBundle)) {
+ return
+ }
+ }
+ SchemaBundle.serialize(schemaBundle, file)
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/DeletionMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/DeletionMethod.kt
new file mode 100644
index 0000000..890367f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/DeletionMethod.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 androidx.room.vo
+
+import javax.lang.model.element.ExecutableElement
+
+class DeletionMethod(
+ element: ExecutableElement,
+ name: String,
+ entities: Map<String, Entity>, returnCount: Boolean,
+ parameters: List<ShortcutQueryParameter>
+) : ShortcutMethod(element, name, entities, returnCount, parameters)
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/EmbeddedField.kt b/room/compiler/src/main/kotlin/androidx/room/vo/EmbeddedField.kt
new file mode 100644
index 0000000..e505396
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/EmbeddedField.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.annotation.NonNull
+import androidx.room.ext.hasAnnotation
+
+/**
+ * Used when a field is embedded inside an Entity or Pojo.
+ */
+// used in cache matching, must stay as a data class or implement equals
+data class EmbeddedField(val field: Field, val prefix: String = "",
+ val parent: EmbeddedField?) {
+ val getter by lazy { field.getter }
+ val setter by lazy { field.setter }
+ val nonNull = field.element.hasAnnotation(NonNull::class)
+ lateinit var pojo: Pojo
+ val mRootParent: EmbeddedField by lazy {
+ parent?.mRootParent ?: this
+ }
+
+ fun isDescendantOf(other: EmbeddedField): Boolean {
+ if (parent == other) {
+ return true
+ } else if (parent == null) {
+ return false
+ } else {
+ return parent.isDescendantOf(other)
+ }
+ }
+
+ fun isNonNullRecursively(): Boolean {
+ return field.nonNull && (parent == null || parent.isNonNullRecursively())
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Entity.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Entity.kt
new file mode 100644
index 0000000..631a924
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Entity.kt
@@ -0,0 +1,99 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.migration.bundle.BundleUtil
+import androidx.room.migration.bundle.EntityBundle
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+
+// TODO make data class when move to kotlin 1.1
+class Entity(
+ element: TypeElement, val tableName: String, type: DeclaredType,
+ fields: List<Field>, embeddedFields: List<EmbeddedField>,
+ val primaryKey: PrimaryKey, val indices: List<Index>,
+ val foreignKeys: List<ForeignKey>,
+ constructor: Constructor?)
+ : Pojo(element, type, fields, embeddedFields, emptyList(), constructor), HasSchemaIdentity {
+
+ val createTableQuery by lazy {
+ createTableQuery(tableName)
+ }
+
+ // a string defining the identity of this entity, which can be used for equality checks
+ override fun getIdKey(): String {
+ val identityKey = SchemaIdentityKey()
+ identityKey.append(tableName)
+ identityKey.append(primaryKey)
+ identityKey.appendSorted(fields)
+ identityKey.appendSorted(indices)
+ identityKey.appendSorted(foreignKeys)
+ return identityKey.hash()
+ }
+
+ private fun createTableQuery(tableName: String): String {
+ val definitions = (fields.map {
+ val autoIncrement = primaryKey.autoGenerateId && primaryKey.fields.contains(it)
+ it.databaseDefinition(autoIncrement)
+ } + createPrimaryKeyDefinition() + createForeignKeyDefinitions()).filterNotNull()
+ return "CREATE TABLE IF NOT EXISTS `$tableName` (${definitions.joinToString(", ")})"
+ }
+
+ private fun createForeignKeyDefinitions(): List<String> {
+ return foreignKeys.map { it.databaseDefinition() }
+ }
+
+ private fun createPrimaryKeyDefinition(): String? {
+ return if (primaryKey.fields.isEmpty() || primaryKey.autoGenerateId) {
+ null
+ } else {
+ val keys = primaryKey.fields
+ .map { "`${it.columnName}`" }
+ .joinToString(", ")
+ "PRIMARY KEY($keys)"
+ }
+ }
+
+ fun shouldBeDeletedAfter(other: Entity): Boolean {
+ return foreignKeys.any {
+ it.parentTable == other.tableName
+ && ((!it.deferred && it.onDelete == ForeignKeyAction.NO_ACTION)
+ || it.onDelete == ForeignKeyAction.RESTRICT)
+ }
+ }
+
+ fun toBundle(): EntityBundle = EntityBundle(
+ tableName,
+ createTableQuery(BundleUtil.TABLE_NAME_PLACEHOLDER),
+ fields.map { it.toBundle() },
+ primaryKey.toBundle(),
+ indices.map { it.toBundle() },
+ foreignKeys.map { it.toBundle() })
+
+ fun isUnique(columns: List<String>): Boolean {
+ return if (primaryKey.columnNames.size == columns.size
+ && primaryKey.columnNames.containsAll(columns)) {
+ true
+ } else {
+ indices.any { index ->
+ index.unique
+ && index.fields.size == columns.size
+ && index.columnNames.containsAll(columns)
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Field.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Field.kt
new file mode 100644
index 0000000..b8b1513
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Field.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.ext.isNonNull
+import androidx.room.ext.typeName
+import androidx.room.migration.bundle.FieldBundle
+import androidx.room.parser.Collate
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.solver.types.CursorValueReader
+import androidx.room.solver.types.StatementValueBinder
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.Element
+import javax.lang.model.type.TypeMirror
+// used in cache matching, must stay as a data class or implement equals
+data class Field(val element: Element, val name: String, val type: TypeMirror,
+ var affinity: SQLTypeAffinity?,
+ val collate: Collate? = null,
+ val columnName: String = name,
+ /* means that this field does not belong to parent, instead, it belongs to a
+ * embedded child of the main Pojo*/
+ val parent: EmbeddedField? = null,
+ // index might be removed when being merged into an Entity
+ var indexed: Boolean = false) : HasSchemaIdentity {
+ lateinit var getter: FieldGetter
+ lateinit var setter: FieldSetter
+ // binds the field into a statement
+ var statementBinder: StatementValueBinder? = null
+ // reads this field from a cursor column
+ var cursorValueReader: CursorValueReader? = null
+ val typeName: TypeName by lazy { type.typeName() }
+
+ /** Whether the table column for this field should be NOT NULL */
+ val nonNull = element.isNonNull() && (parent == null || parent.isNonNullRecursively())
+
+ override fun getIdKey(): String {
+ // we don't get the collate information from sqlite so ignoring it here.
+ return "$columnName-${affinity?.name ?: SQLTypeAffinity.TEXT.name}-$nonNull"
+ }
+
+ /**
+ * Used when reporting errors on duplicate names
+ */
+ fun getPath(): String {
+ return if (parent == null) {
+ name
+ } else {
+ "${parent.field.getPath()} > $name"
+ }
+ }
+
+ private val pathWithDotNotation: String by lazy {
+ if (parent == null) {
+ name
+ } else {
+ "${parent.field.pathWithDotNotation}.$name"
+ }
+ }
+
+ /**
+ * List of names that include variations.
+ * e.g. if it is mUser, user is added to the list
+ * or if it is isAdmin, admin is added to the list
+ */
+ val nameWithVariations by lazy {
+ val result = arrayListOf(name)
+ if (name.length > 1) {
+ if (name.startsWith('_')) {
+ result.add(name.substring(1))
+ }
+ if (name.startsWith("m") && name[1].isUpperCase()) {
+ result.add(name.substring(1).decapitalize())
+ }
+
+ if (typeName == TypeName.BOOLEAN || typeName == TypeName.BOOLEAN.box()) {
+ if (name.length > 2 && name.startsWith("is") && name[2].isUpperCase()) {
+ result.add(name.substring(2).decapitalize())
+ }
+ if (name.length > 3 && name.startsWith("has") && name[3].isUpperCase()) {
+ result.add(name.substring(3).decapitalize())
+ }
+ }
+ }
+ result
+ }
+
+ val getterNameWithVariations by lazy {
+ nameWithVariations.map { "get${it.capitalize()}" } +
+ if (typeName == TypeName.BOOLEAN || typeName == TypeName.BOOLEAN.box()) {
+ nameWithVariations.flatMap {
+ listOf("is${it.capitalize()}", "has${it.capitalize()}")
+ }
+ } else {
+ emptyList()
+ }
+ }
+
+ val setterNameWithVariations by lazy {
+ nameWithVariations.map { "set${it.capitalize()}" }
+ }
+
+ /**
+ * definition to be used in create query
+ */
+ fun databaseDefinition(autoIncrementPKey: Boolean): String {
+ val columnSpec = StringBuilder("")
+ if (autoIncrementPKey) {
+ columnSpec.append(" PRIMARY KEY AUTOINCREMENT")
+ }
+ if (nonNull) {
+ columnSpec.append(" NOT NULL")
+ }
+ if (collate != null) {
+ columnSpec.append(" COLLATE ${collate.name}")
+ }
+ return "`$columnName` ${(affinity ?: SQLTypeAffinity.TEXT).name}$columnSpec"
+ }
+
+ fun toBundle(): FieldBundle = FieldBundle(pathWithDotNotation, columnName,
+ affinity?.name ?: SQLTypeAffinity.TEXT.name, nonNull
+ )
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt b/room/compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
new file mode 100644
index 0000000..74f450b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
+
+data class FieldGetter(val name: String, val type: TypeMirror, val callType: CallType) {
+ fun writeGet(ownerVar: String, outVar: String, builder: CodeBlock.Builder) {
+ val stmt = when (callType) {
+ CallType.FIELD -> "final $T $L = $L.$L"
+ CallType.METHOD -> "final $T $L = $L.$L()"
+ CallType.CONSTRUCTOR -> null
+ }
+ stmt?.let {
+ builder.addStatement(stmt, TypeName.get(type), outVar, ownerVar, name)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/FieldSetter.kt b/room/compiler/src/main/kotlin/androidx/room/vo/FieldSetter.kt
new file mode 100644
index 0000000..7c0001c
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/FieldSetter.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.ext.L
+import com.squareup.javapoet.CodeBlock
+import javax.lang.model.type.TypeMirror
+
+data class FieldSetter(val name: String, val type: TypeMirror, val callType: CallType) {
+ fun writeSet(ownerVar: String, inVar: String, builder: CodeBlock.Builder) {
+ val stmt = when (callType) {
+ CallType.FIELD -> "$L.$L = $L"
+ CallType.METHOD -> "$L.$L($L)"
+ CallType.CONSTRUCTOR -> null
+ }
+ stmt?.let {
+ builder.addStatement(stmt, ownerVar, name, inVar)
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/FieldWithIndex.kt b/room/compiler/src/main/kotlin/androidx/room/vo/FieldWithIndex.kt
new file mode 100644
index 0000000..4a75db0
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/FieldWithIndex.kt
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.room.vo
+
+/**
+ * A common value object when we need to associate a Field with an Index
+ * variable.
+ * <p>
+ * If we are sure that the field will be there at compile time, we set it to always Exists so that
+ * the generated code does not check for -1 column indices.
+ */
+data class FieldWithIndex(val field: Field, val indexVar: String, val alwaysExists: Boolean) {
+ companion object {
+ fun byOrder(fields: List<Field>): List<FieldWithIndex> {
+ return fields.mapIndexed { index, field ->
+ FieldWithIndex(field = field,
+ indexVar = "${index + 1}",
+ alwaysExists = true)
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/ForeignKey.kt b/room/compiler/src/main/kotlin/androidx/room/vo/ForeignKey.kt
new file mode 100644
index 0000000..2481486
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/ForeignKey.kt
@@ -0,0 +1,62 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.migration.bundle.ForeignKeyBundle
+
+/**
+ * Keeps information about a foreign key.
+ */
+data class ForeignKey(val parentTable: String,
+ val parentColumns: List<String>,
+ val childFields: List<Field>,
+ val onDelete: ForeignKeyAction,
+ val onUpdate: ForeignKeyAction,
+ val deferred: Boolean) : HasSchemaIdentity {
+ override fun getIdKey(): String {
+ return parentTable +
+ "-${parentColumns.joinToString(",")}" +
+ "-${childFields.joinToString(",") {it.columnName}}" +
+ "-${onDelete.sqlName}" +
+ "-${onUpdate.sqlName}" +
+ "-$deferred"
+ }
+
+ fun databaseDefinition(): String {
+ return "FOREIGN KEY(${joinEscaped(childFields.map { it.columnName })})" +
+ " REFERENCES `$parentTable`(${joinEscaped(parentColumns)})" +
+ " ON UPDATE ${onUpdate.sqlName}" +
+ " ON DELETE ${onDelete.sqlName}" +
+ " ${deferredDeclaration()}"
+ }
+
+ private fun deferredDeclaration(): String {
+ return if (deferred) {
+ "DEFERRABLE INITIALLY DEFERRED"
+ } else {
+ ""
+ }
+ }
+
+ private fun joinEscaped(values: Iterable<String>) = values.joinToString(", ") { "`$it`" }
+
+ fun toBundle(): ForeignKeyBundle = ForeignKeyBundle(
+ parentTable, onDelete.sqlName, onUpdate.sqlName,
+ childFields.map { it.columnName },
+ parentColumns
+ )
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/ForeignKeyAction.kt b/room/compiler/src/main/kotlin/androidx/room/vo/ForeignKeyAction.kt
new file mode 100644
index 0000000..2d16827
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/ForeignKeyAction.kt
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.ForeignKey
+
+/**
+ * Compiler representation of ForeignKey#Action.
+ */
+enum class ForeignKeyAction(val annotationValue: Int, val sqlName: String) {
+ NO_ACTION(ForeignKey.NO_ACTION, "NO ACTION"),
+ RESTRICT(ForeignKey.RESTRICT, "RESTRICT"),
+ SET_NULL(ForeignKey.SET_NULL, "SET NULL"),
+ SET_DEFAULT(ForeignKey.SET_DEFAULT, "SET DEFAULT"),
+ CASCADE(ForeignKey.CASCADE, "CASCADE");
+ companion object {
+ private val mapping by lazy {
+ ForeignKeyAction.values().associateBy { it.annotationValue }
+ }
+ fun fromAnnotationValue(value: Int?) = mapping[value]
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Index.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Index.kt
new file mode 100644
index 0000000..1bc4c67
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Index.kt
@@ -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 androidx.room.vo
+
+import androidx.room.migration.bundle.BundleUtil
+import androidx.room.migration.bundle.IndexBundle
+
+/**
+ * Represents a processed index.
+ */
+data class Index(val name: String, val unique: Boolean, val fields: List<Field>) :
+ HasSchemaIdentity {
+ companion object {
+ // should match the value in TableInfo.Index.DEFAULT_PREFIX
+ const val DEFAULT_PREFIX = "index_"
+ }
+
+ override fun getIdKey(): String {
+ return "$unique-$name-${fields.joinToString(",") { it.columnName }}"
+ }
+
+ fun createQuery(tableName: String): String {
+ val uniqueSQL = if (unique) {
+ "UNIQUE"
+ } else {
+ ""
+ }
+ return """
+ CREATE $uniqueSQL INDEX `$name`
+ ON `$tableName` (${fields.map { it.columnName }.joinToString(", ") { "`$it`" }})
+ """.trimIndent().replace("\n", " ")
+ }
+
+ val columnNames by lazy { fields.map { it.columnName } }
+
+ fun toBundle(): IndexBundle = IndexBundle(name, unique, fields.map { it.columnName },
+ createQuery(BundleUtil.TABLE_NAME_PLACEHOLDER))
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/InsertionMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/InsertionMethod.kt
new file mode 100644
index 0000000..5bc41d3
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/InsertionMethod.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.OnConflictStrategy
+import androidx.room.ext.typeName
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.TypeMirror
+
+data class InsertionMethod(val element: ExecutableElement, val name: String,
+ @OnConflictStrategy val onConflict: Int,
+ val entities: Map<String, Entity>, val returnType: TypeMirror,
+ val insertionType: Type?,
+ val parameters: List<ShortcutQueryParameter>) {
+ fun insertMethodTypeFor(param: ShortcutQueryParameter): Type {
+ return if (insertionType == Type.INSERT_VOID || insertionType == null) {
+ Type.INSERT_VOID
+ } else if (!param.isMultiple) {
+ Type.INSERT_SINGLE_ID
+ } else {
+ insertionType
+ }
+ }
+
+ enum class Type(
+ // methodName matches EntityInsertionAdapter methods
+ val methodName: String, val returnTypeName: TypeName) {
+ INSERT_VOID("insert", TypeName.VOID), // return void
+ INSERT_SINGLE_ID("insertAndReturnId", TypeName.LONG), // return long
+ INSERT_ID_ARRAY("insertAndReturnIdsArray",
+ ArrayTypeName.of(TypeName.LONG)), // return long[]
+ INSERT_ID_ARRAY_BOX("insertAndReturnIdsArrayBox",
+ ArrayTypeName.of(TypeName.LONG.box())), // return Long[]
+ INSERT_ID_LIST("insertAndReturnIdsList", // return List<Long>
+ ParameterizedTypeName.get(List::class.typeName(), TypeName.LONG.box()))
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
new file mode 100644
index 0000000..5debcc2
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.ext.typeName
+import androidx.room.processor.EntityProcessor
+import com.google.auto.common.MoreElements
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+
+/**
+ * A class is turned into a Pojo if it is used in a query response.
+ */
+open class Pojo(
+ val element: TypeElement,
+ val type: DeclaredType,
+ val fields: List<Field>,
+ val embeddedFields: List<EmbeddedField>,
+ val relations: List<Relation>,
+ val constructor: Constructor? = null) {
+ val typeName: TypeName by lazy { type.typeName() }
+
+ /**
+ * All table names that are somehow accessed by this Pojo.
+ * Might be via Embedded or Relation.
+ */
+ fun accessedTableNames(): List<String> {
+ val entityAnnotation = MoreElements.getAnnotationMirror(element,
+ androidx.room.Entity::class.java).orNull()
+ return if (entityAnnotation != null) {
+ listOf(EntityProcessor.extractTableName(element, entityAnnotation))
+ } else {
+ embeddedFields.flatMap {
+ it.pojo.accessedTableNames()
+ } + relations.map {
+ it.entity.tableName
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/PojoMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/PojoMethod.kt
new file mode 100644
index 0000000..6639025
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/PojoMethod.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018 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 androidx.room.vo
+
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.ExecutableType
+
+/**
+ * An executable element processed as member of a class (pojo or entity)
+ */
+class PojoMethod(
+ val element: ExecutableElement,
+ val resolvedType: ExecutableType,
+ val name: String)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/PrimaryKey.kt b/room/compiler/src/main/kotlin/androidx/room/vo/PrimaryKey.kt
new file mode 100644
index 0000000..95f4a2b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/PrimaryKey.kt
@@ -0,0 +1,43 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.migration.bundle.PrimaryKeyBundle
+import javax.lang.model.element.Element
+
+/**
+ * Represents a PrimaryKey for an Entity.
+ */
+data class PrimaryKey(val declaredIn: Element?, val fields: List<Field>,
+ val autoGenerateId: Boolean) : HasSchemaIdentity {
+ companion object {
+ val MISSING = PrimaryKey(null, emptyList(), false)
+ }
+
+ val columnNames by lazy { fields.map { it.columnName } }
+
+ fun toHumanReadableString(): String {
+ return "PrimaryKey[" +
+ fields.joinToString(separator = ", ", transform = Field::getPath) + "]"
+ }
+
+ fun toBundle(): PrimaryKeyBundle = PrimaryKeyBundle(
+ autoGenerateId, fields.map { it.columnName })
+
+ override fun getIdKey(): String {
+ return "$autoGenerateId-${fields.map { it.columnName }}"
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/QueryMethod.kt
new file mode 100644
index 0000000..9b2c7a1
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/QueryMethod.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.ext.typeName
+import androidx.room.parser.ParsedQuery
+import androidx.room.solver.query.result.QueryResultBinder
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A class that holds information about a QueryMethod.
+ * It is self sufficient and must have all generics etc resolved once created.
+ */
+data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
+ val returnType: TypeMirror, val parameters: List<QueryParameter>,
+ val inTransaction: Boolean,
+ val queryResultBinder: QueryResultBinder) {
+ val sectionToParamMapping by lazy {
+ query.bindSections.map {
+ if (it.text.trim() == "?") {
+ Pair(it, parameters.firstOrNull())
+ } else if (it.text.startsWith(":")) {
+ val subName = it.text.substring(1)
+ Pair(it, parameters.firstOrNull {
+ it.sqlName == subName
+ })
+ } else {
+ Pair(it, null)
+ }
+ }
+ }
+
+ val returnsValue by lazy {
+ returnType.typeName() != TypeName.VOID
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/QueryParameter.kt b/room/compiler/src/main/kotlin/androidx/room/vo/QueryParameter.kt
new file mode 100644
index 0000000..3ac5424
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/QueryParameter.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import androidx.room.solver.query.parameter.QueryParameterAdapter
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Holds the parameter for a {@link QueryMethod}.
+ */
+data class QueryParameter(
+ // this is name seen by java
+ val name: String,
+ // this is the name used in the query. Might be different for kotlin queries
+ val sqlName: String,
+ val type: TypeMirror,
+ val queryParamAdapter: QueryParameterAdapter?)
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt
new file mode 100644
index 0000000..027bd45
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/RawQueryMethod.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 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 androidx.room.vo
+
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.ext.typeName
+import androidx.room.solver.query.result.QueryResultBinder
+import com.squareup.javapoet.TypeName
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A class that holds information about a method annotated with RawQuery.
+ * It is self sufficient and must have all generics etc resolved once created.
+ */
+data class RawQueryMethod(
+ val element: ExecutableElement,
+ val name: String,
+ val returnType: TypeMirror,
+ val inTransaction: Boolean,
+ val observedTableNames: Set<String>,
+ val runtimeQueryParam: RuntimeQueryParameter?,
+ val queryResultBinder: QueryResultBinder) {
+ val returnsValue by lazy {
+ returnType.typeName() != TypeName.VOID
+ }
+
+ data class RuntimeQueryParameter(
+ val paramName: String,
+ val type: TypeName) {
+ fun isString() = CommonTypeNames.STRING == type
+ fun isSupportQuery() = SupportDbTypeNames.QUERY == type
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
new file mode 100644
index 0000000..b97417d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Relation.kt
@@ -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 androidx.room.vo
+
+import androidx.room.ext.typeName
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Value object created from processing a @Relation annotation.
+ */
+class Relation(
+ val entity: Entity,
+ // return type. e..g. String in @Relation List<String>
+ val pojoType: TypeMirror,
+ // field in Pojo that holds these relations (e.g. List<Pet> pets)
+ val field: Field,
+ // the parent field referenced for matching
+ val parentField: Field,
+ // the field referenced for querying. does not need to be in the response but the query
+ // we generate always has it in the response.
+ val entityField: Field,
+ // the projection for the query
+ val projection: List<String>) {
+
+ val pojoTypeName by lazy { pojoType.typeName() }
+
+ fun createLoadAllSql(): String {
+ val resultFields = projection.toSet() + entityField.columnName
+ return createSelect(resultFields)
+ }
+
+ private fun createSelect(resultFields: Set<String>): String {
+ return "SELECT ${resultFields.joinToString(",") {"`$it`"}}" +
+ " FROM `${entity.tableName}`" +
+ " WHERE `${entityField.columnName}` IN (:args)"
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt b/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
new file mode 100644
index 0000000..8523193
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
@@ -0,0 +1,268 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.parser.ParsedQuery
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.parser.SqlParser
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER
+import androidx.room.processor.ProcessorErrors.relationAffinityMismatch
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.query.result.RowAdapter
+import androidx.room.solver.query.result.SingleColumnRowAdapter
+import androidx.room.verifier.DatabaseVerificaitonErrors
+import androidx.room.writer.QueryWriter
+import androidx.room.writer.RelationCollectorMethodWriter
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import stripNonJava
+import java.util.ArrayList
+import java.util.HashSet
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Internal class that is used to manage fetching 1/N to N relationships.
+ */
+data class RelationCollector(val relation: Relation,
+ val affinity: SQLTypeAffinity,
+ val mapTypeName: ParameterizedTypeName,
+ val keyTypeName: TypeName,
+ val collectionTypeName: ParameterizedTypeName,
+ val queryWriter: QueryWriter,
+ val rowAdapter: RowAdapter,
+ val loadAllQuery: ParsedQuery) {
+ // set when writing the code generator in writeInitCode
+ lateinit var varName: String
+
+ fun writeInitCode(scope: CodeGenScope) {
+ val tmpVar = scope.getTmpVar(
+ "_collection${relation.field.getPath().stripNonJava().capitalize()}")
+ scope.builder().addStatement("final $T $L = new $T()", mapTypeName, tmpVar, mapTypeName)
+ varName = tmpVar
+ }
+
+ // called after reading each item to extract the key if it exists
+ fun writeReadParentKeyCode(cursorVarName: String, itemVar: String,
+ fieldsWithIndices: List<FieldWithIndex>, scope: CodeGenScope) {
+ val indexVar = fieldsWithIndices.firstOrNull {
+ it.field === relation.parentField
+ }?.indexVar
+ scope.builder().apply {
+ readKey(
+ cursorVarName = cursorVarName,
+ indexVar = indexVar,
+ scope = scope
+ ) { tmpVar ->
+ val tmpCollectionVar = scope.getTmpVar("_tmpCollection")
+ addStatement("$T $L = $L.get($L)", collectionTypeName, tmpCollectionVar,
+ varName, tmpVar)
+ beginControlFlow("if($L == null)", tmpCollectionVar).apply {
+ addStatement("$L = new $T()", tmpCollectionVar, collectionTypeName)
+ addStatement("$L.put($L, $L)", varName, tmpVar, tmpCollectionVar)
+ }
+ endControlFlow()
+ // set it on the item
+ relation.field.setter.writeSet(itemVar, tmpCollectionVar, this)
+ }
+ }
+ }
+
+ fun writeCollectionCode(scope: CodeGenScope) {
+ val method = scope.writer
+ .getOrCreateMethod(RelationCollectorMethodWriter(this))
+ scope.builder().apply {
+ addStatement("$N($L)", method, varName)
+ }
+ }
+
+ fun readKey(cursorVarName: String, indexVar: String?, scope: CodeGenScope,
+ postRead: CodeBlock.Builder.(String) -> Unit) {
+ val cursorGetter = when (affinity) {
+ SQLTypeAffinity.INTEGER -> "getLong"
+ SQLTypeAffinity.REAL -> "getDouble"
+ SQLTypeAffinity.TEXT -> "getString"
+ SQLTypeAffinity.BLOB -> "getBlob"
+ else -> {
+ "getString"
+ }
+ }
+ scope.builder().apply {
+ beginControlFlow("if (!$L.isNull($L))", cursorVarName, indexVar).apply {
+ val tmpVar = scope.getTmpVar("_tmpKey")
+ addStatement("final $T $L = $L.$L($L)", keyTypeName,
+ tmpVar, cursorVarName, cursorGetter, indexVar)
+ this.postRead(tmpVar)
+ }
+ endControlFlow()
+ }
+ }
+
+ companion object {
+ fun createCollectors(
+ baseContext: Context,
+ relations: List<Relation>
+ ): List<RelationCollector> {
+ return relations.map { relation ->
+ // decide on the affinity
+ val context = baseContext.fork(relation.field.element)
+ val parentAffinity = relation.parentField.cursorValueReader?.affinity()
+ val childAffinity = relation.entityField.cursorValueReader?.affinity()
+ val affinity = if (parentAffinity != null && parentAffinity == childAffinity) {
+ parentAffinity
+ } else {
+ context.logger.w(Warning.RELATION_TYPE_MISMATCH, relation.field.element,
+ relationAffinityMismatch(
+ parentColumn = relation.parentField.columnName,
+ childColumn = relation.entityField.columnName,
+ parentAffinity = parentAffinity,
+ childAffinity = childAffinity))
+ SQLTypeAffinity.TEXT
+ }
+ val keyType = keyTypeFor(context, affinity)
+ val collectionTypeName = if (relation.field.typeName is ParameterizedTypeName) {
+ val paramType = relation.field.typeName as ParameterizedTypeName
+ if (paramType.rawType == CommonTypeNames.LIST) {
+ ParameterizedTypeName.get(ClassName.get(ArrayList::class.java),
+ relation.pojoTypeName)
+ } else if (paramType.rawType == CommonTypeNames.SET) {
+ ParameterizedTypeName.get(ClassName.get(HashSet::class.java),
+ relation.pojoTypeName)
+ } else {
+ ParameterizedTypeName.get(ClassName.get(ArrayList::class.java),
+ relation.pojoTypeName)
+ }
+ } else {
+ ParameterizedTypeName.get(ClassName.get(ArrayList::class.java),
+ relation.pojoTypeName)
+ }
+
+ val canUseArrayMap = context.processingEnv.elementUtils
+ .getTypeElement(AndroidTypeNames.ARRAY_MAP.toString()) != null
+ val mapClass = if (canUseArrayMap) {
+ AndroidTypeNames.ARRAY_MAP
+ } else {
+ ClassName.get(java.util.HashMap::class.java)
+ }
+ val tmpMapType = ParameterizedTypeName.get(mapClass, keyType, collectionTypeName)
+ val keyTypeMirror = keyTypeMirrorFor(context, affinity)
+ val set = context.processingEnv.elementUtils.getTypeElement("java.util.Set")
+ val keySet = context.processingEnv.typeUtils.getDeclaredType(set, keyTypeMirror)
+ val loadAllQuery = relation.createLoadAllSql()
+ val parsedQuery = SqlParser.parse(loadAllQuery)
+ context.checker.check(parsedQuery.errors.isEmpty(), relation.field.element,
+ parsedQuery.errors.joinToString("\n"))
+ if (parsedQuery.errors.isEmpty()) {
+ val resultInfo = context.databaseVerifier?.analyze(loadAllQuery)
+ parsedQuery.resultInfo = resultInfo
+ if (resultInfo?.error != null) {
+ context.logger.e(relation.field.element,
+ DatabaseVerificaitonErrors.cannotVerifyQuery(resultInfo.error))
+ }
+ }
+ val resultInfo = parsedQuery.resultInfo
+
+ val queryParam = QueryParameter(
+ name = RelationCollectorMethodWriter.KEY_SET_VARIABLE,
+ sqlName = RelationCollectorMethodWriter.KEY_SET_VARIABLE,
+ type = keySet,
+ queryParamAdapter =
+ context.typeAdapterStore.findQueryParameterAdapter(keySet))
+ val queryWriter = QueryWriter(
+ parameters = listOf(queryParam),
+ sectionToParamMapping = listOf(Pair(parsedQuery.bindSections.first(),
+ queryParam)),
+ query = parsedQuery
+ )
+
+ // row adapter that matches full response
+ fun getDefaultRowAdapter(): RowAdapter? {
+ return context.typeAdapterStore.findRowAdapter(relation.pojoType, parsedQuery)
+ }
+ val rowAdapter = if (relation.projection.size == 1 && resultInfo != null &&
+ (resultInfo.columns.size == 1 || resultInfo.columns.size == 2)) {
+ // check for a column adapter first
+ val cursorReader = context.typeAdapterStore.findCursorValueReader(
+ relation.pojoType, resultInfo.columns.first().type)
+ if (cursorReader == null) {
+ getDefaultRowAdapter()
+ } else {
+ context.logger.d("Choosing cursor adapter for the return value since" +
+ " the query returns only 1 or 2 columns and there is a cursor" +
+ " adapter for the return type.")
+ SingleColumnRowAdapter(cursorReader)
+ }
+ } else {
+ getDefaultRowAdapter()
+ }
+
+ if (rowAdapter == null) {
+ context.logger.e(relation.field.element, CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ null
+ } else {
+ RelationCollector(
+ relation = relation,
+ affinity = affinity,
+ mapTypeName = tmpMapType,
+ keyTypeName = keyType,
+ collectionTypeName = collectionTypeName,
+ queryWriter = queryWriter,
+ rowAdapter = rowAdapter,
+ loadAllQuery = parsedQuery
+ )
+ }
+ }.filterNotNull()
+ }
+
+ private fun keyTypeMirrorFor(context: Context, affinity: SQLTypeAffinity): TypeMirror {
+ val types = context.processingEnv.typeUtils
+ val elements = context.processingEnv.elementUtils
+ return when (affinity) {
+ SQLTypeAffinity.INTEGER -> elements.getTypeElement("java.lang.Long").asType()
+ SQLTypeAffinity.REAL -> elements.getTypeElement("java.lang.Double").asType()
+ SQLTypeAffinity.TEXT -> context.COMMON_TYPES.STRING
+ SQLTypeAffinity.BLOB -> types.getArrayType(types.getPrimitiveType(TypeKind.BYTE))
+ else -> {
+ context.COMMON_TYPES.STRING
+ }
+ }
+ }
+
+ private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): TypeName {
+ return when (affinity) {
+ SQLTypeAffinity.INTEGER -> TypeName.LONG.box()
+ SQLTypeAffinity.REAL -> TypeName.DOUBLE.box()
+ SQLTypeAffinity.TEXT -> TypeName.get(String::class.java)
+ SQLTypeAffinity.BLOB -> ArrayTypeName.of(TypeName.BYTE)
+ else -> {
+ // no affinity select from type
+ context.COMMON_TYPES.STRING.typeName()
+ }
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/SchemaIdentityKey.kt b/room/compiler/src/main/kotlin/androidx/room/vo/SchemaIdentityKey.kt
new file mode 100644
index 0000000..9a241fa
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/SchemaIdentityKey.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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 androidx.room.vo
+
+import org.apache.commons.codec.digest.DigestUtils
+import java.util.Locale
+
+interface HasSchemaIdentity {
+ fun getIdKey(): String
+}
+
+/**
+ * A class that can be converted into a unique identifier for an object
+ */
+class SchemaIdentityKey {
+ companion object {
+ private val SEPARATOR = "?:?"
+ private val ENGLISH_SORT = Comparator<String> { o1, o2 ->
+ o1.toLowerCase(Locale.ENGLISH).compareTo(o2.toLowerCase(Locale.ENGLISH))
+ }
+ }
+
+ private val sb = StringBuilder()
+ fun append(identity: HasSchemaIdentity) {
+ append(identity.getIdKey())
+ }
+
+ fun appendSorted(identities: List<HasSchemaIdentity>) {
+ identities.map { it.getIdKey() }.sortedWith(ENGLISH_SORT).forEach {
+ append(it)
+ }
+ }
+
+ fun hash() = DigestUtils.md5Hex(sb.toString())
+ fun append(identity: String) {
+ sb.append(identity).append(SEPARATOR)
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/ShortcutMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/ShortcutMethod.kt
new file mode 100644
index 0000000..dcaa78d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/ShortcutMethod.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 androidx.room.vo
+
+import javax.lang.model.element.ExecutableElement
+
+/**
+ * Base class for shortcut methods in @DAO.
+ */
+abstract class ShortcutMethod(val element: ExecutableElement, val name: String,
+ val entities: Map<String, Entity>, val returnCount: Boolean,
+ val parameters: List<ShortcutQueryParameter>)
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/ShortcutQueryParameter.kt b/room/compiler/src/main/kotlin/androidx/room/vo/ShortcutQueryParameter.kt
new file mode 100644
index 0000000..d33f3e0
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/ShortcutQueryParameter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.vo
+
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Parameters used in DAO methods that are annotated with Insert, Delete, Update.
+ */
+data class ShortcutQueryParameter(val name: String, val type: TypeMirror,
+ val entityType: TypeMirror?, val isMultiple: Boolean) {
+ /**
+ * Method name in entity insertion or update adapter.
+ */
+ fun handleMethodName(): String {
+ return if (isMultiple) {
+ "handleMultiple"
+ } else {
+ "handle"
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/TransactionMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/TransactionMethod.kt
new file mode 100644
index 0000000..9457f43
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/TransactionMethod.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 androidx.room.vo
+
+import javax.lang.model.element.ExecutableElement
+
+class TransactionMethod(val element: ExecutableElement, val name: String, val callType: CallType) {
+ enum class CallType {
+ CONCRETE, DEFAULT_JAVA8, DEFAULT_KOTLIN
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/UpdateMethod.kt b/room/compiler/src/main/kotlin/androidx/room/vo/UpdateMethod.kt
new file mode 100644
index 0000000..08b11f4
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/UpdateMethod.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.OnConflictStrategy
+import javax.lang.model.element.ExecutableElement
+
+class UpdateMethod(element: ExecutableElement, name: String,
+ entities: Map<String, Entity>, returnCount: Boolean,
+ parameters: List<ShortcutQueryParameter>,
+ @OnConflictStrategy val onConflictStrategy: Int) : ShortcutMethod(
+ element, name, entities, returnCount, parameters)
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
new file mode 100644
index 0000000..561f96d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -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 androidx.room.vo
+
+/**
+ * Internal representation of supported warnings
+ */
+enum class Warning(val publicKey: String) {
+ ALL("ALL"),
+ CURSOR_MISMATCH("ROOM_CURSOR_MISMATCH"),
+ MISSING_JAVA_TMP_DIR("ROOM_MISSING_JAVA_TMP_DIR"),
+ CANNOT_CREATE_VERIFICATION_DATABASE("ROOM_CANNOT_CREATE_VERIFICATION_DATABASE"),
+ PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED("ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED"),
+ INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED("ROOM_EMBEDDED_INDEX_IS_DROPPED"),
+ INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED("ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED"),
+ INDEX_FROM_PARENT_IS_DROPPED("ROOM_PARENT_INDEX_IS_DROPPED"),
+ INDEX_FROM_PARENT_FIELD_IS_DROPPED("ROOM_PARENT_FIELD_INDEX_IS_DROPPED"),
+ RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH"),
+ MISSING_SCHEMA_LOCATION("ROOM_MISSING_SCHEMA_LOCATION"),
+ MISSING_INDEX_ON_FOREIGN_KEY_CHILD("ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"),
+ RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
+ DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR");
+
+ companion object {
+ val PUBLIC_KEY_MAP = Warning.values().associateBy { it.publicKey }
+ fun fromPublicKey(publicKey: String): Warning? {
+ return PUBLIC_KEY_MAP[publicKey.toUpperCase()]
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/ClassWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/ClassWriter.kt
new file mode 100644
index 0000000..a7ebef2
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/ClassWriter.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.RoomProcessor
+import androidx.room.ext.S
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope.Companion.CLASS_PROPERTY_PREFIX
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.annotation.processing.ProcessingEnvironment
+
+/**
+ * Base class for all writers that can produce a class.
+ */
+abstract class ClassWriter(private val className: ClassName) {
+ private val sharedFieldSpecs = mutableMapOf<String, FieldSpec>()
+ private val sharedMethodSpecs = mutableMapOf<String, MethodSpec>()
+ private val sharedFieldNames = mutableSetOf<String>()
+ private val sharedMethodNames = mutableSetOf<String>()
+
+ abstract fun createTypeSpecBuilder(): TypeSpec.Builder
+
+ fun write(processingEnv: ProcessingEnvironment) {
+ val builder = createTypeSpecBuilder()
+ sharedFieldSpecs.values.forEach { builder.addField(it) }
+ sharedMethodSpecs.values.forEach { builder.addMethod(it) }
+ addGeneratedAnnotationIfAvailable(builder, processingEnv)
+ addSuppressUnchecked(builder)
+ JavaFile.builder(className.packageName(), builder.build())
+ .build()
+ .writeTo(processingEnv.filer)
+ }
+
+ private fun addSuppressUnchecked(builder: TypeSpec.Builder) {
+ val suppressSpec = AnnotationSpec.builder(SuppressWarnings::class.typeName()).addMember(
+ "value",
+ S,
+ "unchecked"
+ ).build()
+ builder.addAnnotation(suppressSpec)
+ }
+
+ private fun addGeneratedAnnotationIfAvailable(adapterTypeSpecBuilder: TypeSpec.Builder,
+ processingEnv: ProcessingEnvironment) {
+ val generatedAnnotationAvailable = processingEnv
+ .elementUtils
+ .getTypeElement(GENERATED_PACKAGE + "." + GENERATED_NAME) != null
+ if (generatedAnnotationAvailable) {
+ val className = ClassName.get(GENERATED_PACKAGE, GENERATED_NAME)
+ val generatedAnnotationSpec =
+ AnnotationSpec.builder(className).addMember(
+ "value",
+ S,
+ RoomProcessor::class.java.canonicalName).build()
+ adapterTypeSpecBuilder.addAnnotation(generatedAnnotationSpec)
+ }
+ }
+
+ private fun makeUnique(set: MutableSet<String>, value: String): String {
+ if (!value.startsWith(CLASS_PROPERTY_PREFIX)) {
+ return makeUnique(set, "$CLASS_PROPERTY_PREFIX$value")
+ }
+ if (set.add(value)) {
+ return value
+ }
+ var index = 1
+ while (true) {
+ if (set.add("${value}_$index")) {
+ return "${value}_$index"
+ }
+ index++
+ }
+ }
+
+ fun getOrCreateField(sharedField: SharedFieldSpec): FieldSpec {
+ return sharedFieldSpecs.getOrPut(sharedField.getUniqueKey(), {
+ sharedField.build(this, makeUnique(sharedFieldNames, sharedField.baseName))
+ })
+ }
+
+ fun getOrCreateMethod(sharedMethod: SharedMethodSpec): MethodSpec {
+ return sharedMethodSpecs.getOrPut(sharedMethod.getUniqueKey(), {
+ sharedMethod.build(this, makeUnique(sharedMethodNames, sharedMethod.baseName))
+ })
+ }
+
+ abstract class SharedFieldSpec(val baseName: String, val type: TypeName) {
+
+ abstract fun getUniqueKey(): String
+
+ abstract fun prepare(writer: ClassWriter, builder: FieldSpec.Builder)
+
+ fun build(classWriter: ClassWriter, name: String): FieldSpec {
+ val builder = FieldSpec.builder(type, name)
+ prepare(classWriter, builder)
+ return builder.build()
+ }
+ }
+
+ abstract class SharedMethodSpec(val baseName: String) {
+
+ abstract fun getUniqueKey(): String
+ abstract fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder)
+
+ fun build(writer: ClassWriter, name: String): MethodSpec {
+ val builder = MethodSpec.methodBuilder(name)
+ prepare(name, writer, builder)
+ return builder.build()
+ }
+ }
+
+ companion object {
+ private const val GENERATED_PACKAGE = "javax.annotation"
+ private const val GENERATED_NAME = "Generated"
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
new file mode 100644
index 0000000..5b6fd10
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.parser.QueryType
+import androidx.room.processor.OnConflictProcessor
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Dao
+import androidx.room.vo.Entity
+import androidx.room.vo.InsertionMethod
+import androidx.room.vo.QueryMethod
+import androidx.room.vo.RawQueryMethod
+import androidx.room.vo.ShortcutMethod
+import androidx.room.vo.TransactionMethod
+import com.google.auto.common.MoreTypes
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import org.jetbrains.kotlin.load.java.JvmAbi
+import stripNonJava
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier.FINAL
+import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.element.Modifier.PUBLIC
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+
+/**
+ * Creates the implementation for a class annotated with Dao.
+ */
+class DaoWriter(val dao: Dao, val processingEnv: ProcessingEnvironment)
+ : ClassWriter(dao.typeName) {
+ private val declaredDao = MoreTypes.asDeclared(dao.element.asType())
+
+ companion object {
+ // TODO nothing prevents this from conflicting, we should fix.
+ val dbField: FieldSpec = FieldSpec
+ .builder(RoomTypeNames.ROOM_DB, "__db", PRIVATE, FINAL)
+ .build()
+
+ private fun typeNameToFieldName(typeName: TypeName?): String {
+ if (typeName is ClassName) {
+ return typeName.simpleName()
+ } else {
+ return typeName.toString().replace('.', '_').stripNonJava()
+ }
+ }
+ }
+
+ override fun createTypeSpecBuilder(): TypeSpec.Builder {
+ val builder = TypeSpec.classBuilder(dao.implTypeName)
+ /**
+ * if delete / update query method wants to return modified rows, we need prepared query.
+ * in that case, if args are dynamic, we cannot re-use the query, if not, we should re-use
+ * it. this requires more work but creates good performance.
+ */
+ val groupedDeleteUpdate = dao.queryMethods
+ .filter { it.query.type == QueryType.DELETE || it.query.type == QueryType.UPDATE }
+ .groupBy { it.parameters.any { it.queryParamAdapter?.isMultiple ?: true } }
+ // delete queries that can be prepared ahead of time
+ val preparedDeleteOrUpdateQueries = groupedDeleteUpdate[false] ?: emptyList()
+ // delete queries that must be rebuild every single time
+ val oneOffDeleteOrUpdateQueries = groupedDeleteUpdate[true] ?: emptyList()
+ val shortcutMethods = createInsertionMethods() +
+ createDeletionMethods() + createUpdateMethods() + createTransactionMethods() +
+ createPreparedDeleteOrUpdateQueries(preparedDeleteOrUpdateQueries)
+
+ builder.apply {
+ addModifiers(PUBLIC)
+ if (dao.element.kind == ElementKind.INTERFACE) {
+ addSuperinterface(dao.typeName)
+ } else {
+ superclass(dao.typeName)
+ }
+ addField(dbField)
+ val dbParam = ParameterSpec
+ .builder(dao.constructorParamType ?: dbField.type, dbField.name).build()
+
+ addMethod(createConstructor(dbParam, shortcutMethods, dao.constructorParamType != null))
+
+ shortcutMethods.forEach {
+ addMethod(it.methodImpl)
+ }
+
+ dao.queryMethods.filter { it.query.type == QueryType.SELECT }.forEach { method ->
+ addMethod(createSelectMethod(method))
+ }
+ oneOffDeleteOrUpdateQueries.forEach {
+ addMethod(createDeleteOrUpdateQueryMethod(it))
+ }
+ dao.rawQueryMethods.forEach {
+ addMethod(createRawQueryMethod(it))
+ }
+ }
+ return builder
+ }
+
+ private fun createPreparedDeleteOrUpdateQueries(
+ preparedDeleteQueries: List<QueryMethod>): List<PreparedStmtQuery> {
+ return preparedDeleteQueries.map { method ->
+ val fieldSpec = getOrCreateField(PreparedStatementField(method))
+ val queryWriter = QueryWriter(method)
+ val fieldImpl = PreparedStatementWriter(queryWriter)
+ .createAnonymous(this@DaoWriter, dbField)
+ val methodBody = createPreparedDeleteQueryMethodBody(method, fieldSpec, queryWriter)
+ PreparedStmtQuery(mapOf(PreparedStmtQuery.NO_PARAM_FIELD
+ to (fieldSpec to fieldImpl)), methodBody)
+ }
+ }
+
+ private fun createPreparedDeleteQueryMethodBody(
+ method: QueryMethod,
+ preparedStmtField: FieldSpec,
+ queryWriter: QueryWriter
+ ): MethodSpec {
+ val scope = CodeGenScope(this)
+ val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
+ val stmtName = scope.getTmpVar("_stmt")
+ addStatement("final $T $L = $N.acquire()",
+ SupportDbTypeNames.SQLITE_STMT, stmtName, preparedStmtField)
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try").apply {
+ val bindScope = scope.fork()
+ queryWriter.bindArgs(stmtName, emptyList(), bindScope)
+ addCode(bindScope.builder().build())
+ if (method.returnsValue) {
+ val resultVar = scope.getTmpVar("_result")
+ addStatement("final $L $L = $L.executeUpdateDelete()",
+ method.returnType.typeName(), resultVar, stmtName)
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ addStatement("return $L", resultVar)
+ } else {
+ addStatement("$L.executeUpdateDelete()", stmtName)
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ }
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$N.endTransaction()", dbField)
+ addStatement("$N.release($L)", preparedStmtField, stmtName)
+ }
+ endControlFlow()
+ }
+ return methodBuilder.build()
+ }
+
+ private fun createTransactionMethods(): List<PreparedStmtQuery> {
+ return dao.transactionMethods.map {
+ PreparedStmtQuery(emptyMap(), createTransactionMethodBody(it))
+ }
+ }
+
+ private fun createTransactionMethodBody(method: TransactionMethod): MethodSpec {
+ val scope = CodeGenScope(this)
+ val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try").apply {
+ val returnsValue = method.element.returnType.kind != TypeKind.VOID
+ val resultVar = if (returnsValue) {
+ scope.getTmpVar("_result")
+ } else {
+ null
+ }
+ addDelegateToSuperStatement(method.element, method.callType, resultVar)
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ if (returnsValue) {
+ addStatement("return $N", resultVar)
+ }
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$N.endTransaction()", dbField)
+ }
+ endControlFlow()
+ }
+ return methodBuilder.build()
+ }
+
+ private fun MethodSpec.Builder.addDelegateToSuperStatement(
+ element: ExecutableElement,
+ callType: TransactionMethod.CallType,
+ result: String?) {
+ val params: MutableList<Any> = mutableListOf()
+ val format = buildString {
+ if (result != null) {
+ append("$T $L = ")
+ params.add(element.returnType)
+ params.add(result)
+ }
+ when (callType) {
+ TransactionMethod.CallType.CONCRETE -> {
+ append("super.$N(")
+ params.add(element.simpleName)
+ }
+ TransactionMethod.CallType.DEFAULT_JAVA8 -> {
+ append("$N.super.$N(")
+ params.add(element.enclosingElement.simpleName)
+ params.add(element.simpleName)
+ }
+ TransactionMethod.CallType.DEFAULT_KOTLIN -> {
+ append("$N.$N.$N(this, ")
+ params.add(element.enclosingElement.simpleName)
+ params.add(JvmAbi.DEFAULT_IMPLS_CLASS_NAME)
+ params.add(element.simpleName)
+ }
+ }
+ var first = true
+ element.parameters.forEach {
+ if (first) {
+ first = false
+ } else {
+ append(", ")
+ }
+ append(L)
+ params.add(it.simpleName)
+ }
+ append(")")
+ }
+ addStatement(format, *params.toTypedArray())
+ }
+
+ private fun createConstructor(
+ dbParam: ParameterSpec,
+ shortcutMethods: List<PreparedStmtQuery>,
+ callSuper: Boolean): MethodSpec {
+ return MethodSpec.constructorBuilder().apply {
+ addParameter(dbParam)
+ addModifiers(PUBLIC)
+ if (callSuper) {
+ addStatement("super($N)", dbParam)
+ }
+ addStatement("this.$N = $N", dbField, dbParam)
+ shortcutMethods.filterNot {
+ it.fields.isEmpty()
+ }.map {
+ it.fields.values
+ }.flatten().groupBy {
+ it.first.name
+ }.map {
+ it.value.first()
+ }.forEach {
+ addStatement("this.$N = $L", it.first, it.second)
+ }
+ }.build()
+ }
+
+ private fun createSelectMethod(method: QueryMethod): MethodSpec {
+ return overrideWithoutAnnotations(method.element, declaredDao).apply {
+ addCode(createQueryMethodBody(method))
+ }.build()
+ }
+
+ private fun createRawQueryMethod(method: RawQueryMethod): MethodSpec {
+ return overrideWithoutAnnotations(method.element, declaredDao).apply {
+ val scope = CodeGenScope(this@DaoWriter)
+ val roomSQLiteQueryVar: String
+ val queryParam = method.runtimeQueryParam
+ val shouldReleaseQuery: Boolean
+
+ when {
+ queryParam?.isString() == true -> {
+ roomSQLiteQueryVar = scope.getTmpVar("_statement")
+ shouldReleaseQuery = true
+ addStatement("$T $L = $T.acquire($L, 0)",
+ RoomTypeNames.ROOM_SQL_QUERY,
+ roomSQLiteQueryVar,
+ RoomTypeNames.ROOM_SQL_QUERY,
+ queryParam.paramName)
+ }
+ queryParam?.isSupportQuery() == true -> {
+ shouldReleaseQuery = false
+ roomSQLiteQueryVar = scope.getTmpVar("_internalQuery")
+ // move it to a final variable so that the generated code can use it inside
+ // callback blocks in java 7
+ addStatement("final $T $L = $N",
+ queryParam.type,
+ roomSQLiteQueryVar,
+ queryParam.paramName)
+ }
+ else -> {
+ // try to generate compiling code. we would've already reported this error
+ roomSQLiteQueryVar = scope.getTmpVar("_statement")
+ shouldReleaseQuery = false
+ addStatement("$T $L = $T.acquire($L, 0)",
+ RoomTypeNames.ROOM_SQL_QUERY,
+ roomSQLiteQueryVar,
+ RoomTypeNames.ROOM_SQL_QUERY,
+ "missing query parameter")
+ }
+ }
+ if (method.returnsValue) {
+ // don't generate code because it will create 1 more error. The original error is
+ // already reported by the processor.
+ method.queryResultBinder.convertAndReturn(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = shouldReleaseQuery,
+ dbField = dbField,
+ inTransaction = method.inTransaction,
+ scope = scope)
+ }
+ addCode(scope.builder().build())
+ }.build()
+ }
+
+ private fun createDeleteOrUpdateQueryMethod(method: QueryMethod): MethodSpec {
+ return overrideWithoutAnnotations(method.element, declaredDao).apply {
+ addCode(createDeleteOrUpdateQueryMethodBody(method))
+ }.build()
+ }
+
+ /**
+ * Groups all insertion methods based on the insert statement they will use then creates all
+ * field specs, EntityInsertionAdapterWriter and actual insert methods.
+ */
+ private fun createInsertionMethods(): List<PreparedStmtQuery> {
+ return dao.insertionMethods
+ .map { insertionMethod ->
+ val onConflict = OnConflictProcessor.onConflictText(insertionMethod.onConflict)
+ val entities = insertionMethod.entities
+
+ val fields = entities.mapValues {
+ val spec = getOrCreateField(InsertionMethodField(it.value, onConflict))
+ val impl = EntityInsertionAdapterWriter(it.value, onConflict)
+ .createAnonymous(this@DaoWriter, dbField.name)
+ spec to impl
+ }
+ val methodImpl = overrideWithoutAnnotations(insertionMethod.element,
+ declaredDao).apply {
+ addCode(createInsertionMethodBody(insertionMethod, fields))
+ }.build()
+ PreparedStmtQuery(fields, methodImpl)
+ }.filterNotNull()
+ }
+
+ private fun createInsertionMethodBody(
+ method: InsertionMethod,
+ insertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>
+ ): CodeBlock {
+ val insertionType = method.insertionType
+ if (insertionAdapters.isEmpty() || insertionType == null) {
+ return CodeBlock.builder().build()
+ }
+ val scope = CodeGenScope(this)
+
+ return scope.builder().apply {
+ // TODO assert thread
+ // TODO collect results
+ addStatement("$N.beginTransaction()", dbField)
+ val needsReturnType = insertionType != InsertionMethod.Type.INSERT_VOID
+ val resultVar = if (needsReturnType) {
+ scope.getTmpVar("_result")
+ } else {
+ null
+ }
+
+ beginControlFlow("try").apply {
+ method.parameters.forEach { param ->
+ val insertionAdapter = insertionAdapters[param.name]?.first
+ if (needsReturnType) {
+ // if it has more than 1 parameter, we would've already printed the error
+ // so we don't care about re-declaring the variable here
+ addStatement("$T $L = $N.$L($L)",
+ insertionType.returnTypeName, resultVar,
+ insertionAdapter, insertionType.methodName,
+ param.name)
+ } else {
+ addStatement("$N.$L($L)", insertionAdapter, insertionType.methodName,
+ param.name)
+ }
+ }
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ if (needsReturnType) {
+ addStatement("return $L", resultVar)
+ }
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$N.endTransaction()", dbField)
+ }
+ endControlFlow()
+ }.build()
+ }
+
+ /**
+ * Creates EntityUpdateAdapter for each deletion method.
+ */
+ private fun createDeletionMethods(): List<PreparedStmtQuery> {
+ return createShortcutMethods(dao.deletionMethods, "deletion", { _, entity ->
+ EntityDeletionAdapterWriter(entity)
+ .createAnonymous(this@DaoWriter, dbField.name)
+ })
+ }
+
+ /**
+ * Creates EntityUpdateAdapter for each @Update method.
+ */
+ private fun createUpdateMethods(): List<PreparedStmtQuery> {
+ return createShortcutMethods(dao.updateMethods, "update", { update, entity ->
+ val onConflict = OnConflictProcessor.onConflictText(update.onConflictStrategy)
+ EntityUpdateAdapterWriter(entity, onConflict)
+ .createAnonymous(this@DaoWriter, dbField.name)
+ })
+ }
+
+ private fun <T : ShortcutMethod> createShortcutMethods(
+ methods: List<T>, methodPrefix: String,
+ implCallback: (T, Entity) -> TypeSpec
+ ): List<PreparedStmtQuery> {
+ return methods.map { method ->
+ val entities = method.entities
+
+ if (entities.isEmpty()) {
+ null
+ } else {
+ val fields = entities.mapValues {
+ val spec = getOrCreateField(DeleteOrUpdateAdapterField(it.value, methodPrefix))
+ val impl = implCallback(method, it.value)
+ spec to impl
+ }
+ val methodSpec = overrideWithoutAnnotations(method.element, declaredDao).apply {
+ addCode(createDeleteOrUpdateMethodBody(method, fields))
+ }.build()
+ PreparedStmtQuery(fields, methodSpec)
+ }
+ }.filterNotNull()
+ }
+
+ private fun createDeleteOrUpdateMethodBody(
+ method: ShortcutMethod,
+ adapters: Map<String, Pair<FieldSpec, TypeSpec>>
+ ): CodeBlock {
+ if (adapters.isEmpty()) {
+ return CodeBlock.builder().build()
+ }
+ val scope = CodeGenScope(this)
+ val resultVar = if (method.returnCount) {
+ scope.getTmpVar("_total")
+ } else {
+ null
+ }
+ return scope.builder().apply {
+ if (resultVar != null) {
+ addStatement("$T $L = 0", TypeName.INT, resultVar)
+ }
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try").apply {
+ method.parameters.forEach { param ->
+ val adapter = adapters[param.name]?.first
+ addStatement("$L$N.$L($L)",
+ if (resultVar == null) "" else "$resultVar +=",
+ adapter, param.handleMethodName(), param.name)
+ }
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ if (resultVar != null) {
+ addStatement("return $L", resultVar)
+ }
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$N.endTransaction()", dbField)
+ }
+ endControlFlow()
+ }.build()
+ }
+
+ /**
+ * @Query with delete action
+ */
+ private fun createDeleteOrUpdateQueryMethodBody(method: QueryMethod): CodeBlock {
+ val queryWriter = QueryWriter(method)
+ val scope = CodeGenScope(this)
+ val sqlVar = scope.getTmpVar("_sql")
+ val stmtVar = scope.getTmpVar("_stmt")
+ val listSizeArgs = queryWriter.prepareQuery(sqlVar, scope)
+ scope.builder().apply {
+ addStatement("$T $L = $N.compileStatement($L)",
+ SupportDbTypeNames.SQLITE_STMT, stmtVar, dbField, sqlVar)
+ queryWriter.bindArgs(stmtVar, listSizeArgs, scope)
+ addStatement("$N.beginTransaction()", dbField)
+ beginControlFlow("try").apply {
+ if (method.returnsValue) {
+ val resultVar = scope.getTmpVar("_result")
+ addStatement("final $L $L = $L.executeUpdateDelete()",
+ method.returnType.typeName(), resultVar, stmtVar)
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ addStatement("return $L", resultVar)
+ } else {
+ addStatement("$L.executeUpdateDelete()", stmtVar)
+ addStatement("$N.setTransactionSuccessful()", dbField)
+ }
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$N.endTransaction()", dbField)
+ }
+ endControlFlow()
+ }
+ return scope.builder().build()
+ }
+
+ private fun createQueryMethodBody(method: QueryMethod): CodeBlock {
+ val queryWriter = QueryWriter(method)
+ val scope = CodeGenScope(this)
+ val sqlVar = scope.getTmpVar("_sql")
+ val roomSQLiteQueryVar = scope.getTmpVar("_statement")
+ queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
+ method.queryResultBinder.convertAndReturn(
+ roomSQLiteQueryVar = roomSQLiteQueryVar,
+ canReleaseQuery = true,
+ dbField = dbField,
+ inTransaction = method.inTransaction,
+ scope = scope)
+ return scope.builder().build()
+ }
+
+ private fun overrideWithoutAnnotations(
+ elm: ExecutableElement,
+ owner: DeclaredType): MethodSpec.Builder {
+ val baseSpec = MethodSpec.overriding(elm, owner, processingEnv.typeUtils).build()
+ return MethodSpec.methodBuilder(baseSpec.name).apply {
+ addAnnotation(Override::class.java)
+ addModifiers(baseSpec.modifiers)
+ addParameters(baseSpec.parameters)
+ varargs(baseSpec.varargs)
+ returns(baseSpec.returnType)
+ }
+ }
+
+ /**
+ * Represents a query statement prepared in Dao implementation.
+ *
+ * @param fields This map holds all the member fields necessary for this query. The key is the
+ * corresponding parameter name in the defining query method. The value is a pair from the field
+ * declaration to definition.
+ * @param methodImpl The body of the query method implementation.
+ */
+ data class PreparedStmtQuery(
+ val fields: Map<String, Pair<FieldSpec, TypeSpec>>,
+ val methodImpl: MethodSpec) {
+ companion object {
+ // The key to be used in `fields` where the method requires a field that is not
+ // associated with any of its parameters
+ const val NO_PARAM_FIELD = "-"
+ }
+ }
+
+ private class InsertionMethodField(val entity: Entity, val onConflictText: String)
+ : SharedFieldSpec(
+ "insertionAdapterOf${Companion.typeNameToFieldName(entity.typeName)}",
+ RoomTypeNames.INSERTION_ADAPTER) {
+
+ override fun getUniqueKey(): String {
+ return "${entity.typeName} $onConflictText"
+ }
+
+ override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
+ builder.addModifiers(FINAL, PRIVATE)
+ }
+ }
+
+ class DeleteOrUpdateAdapterField(val entity: Entity, val methodPrefix: String)
+ : SharedFieldSpec(
+ "${methodPrefix}AdapterOf${Companion.typeNameToFieldName(entity.typeName)}",
+ RoomTypeNames.DELETE_OR_UPDATE_ADAPTER) {
+ override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
+ builder.addModifiers(PRIVATE, FINAL)
+ }
+
+ override fun getUniqueKey(): String {
+ return entity.typeName.toString() + methodPrefix
+ }
+ }
+
+ class PreparedStatementField(val method: QueryMethod) : SharedFieldSpec(
+ "preparedStmtOf${method.name.capitalize()}", RoomTypeNames.SHARED_SQLITE_STMT) {
+ override fun prepare(writer: ClassWriter, builder: FieldSpec.Builder) {
+ builder.addModifiers(PRIVATE, FINAL)
+ }
+
+ override fun getUniqueKey(): String {
+ return method.query.original
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
new file mode 100644
index 0000000..4acd316
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.DaoMethod
+import androidx.room.vo.Database
+import com.google.auto.common.MoreElements
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import stripNonJava
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.element.Modifier.PROTECTED
+import javax.lang.model.element.Modifier.PUBLIC
+import javax.lang.model.element.Modifier.VOLATILE
+
+/**
+ * Writes implementation of classes that were annotated with @Database.
+ */
+class DatabaseWriter(val database: Database) : ClassWriter(database.implTypeName) {
+ override fun createTypeSpecBuilder(): TypeSpec.Builder {
+ val builder = TypeSpec.classBuilder(database.implTypeName)
+ builder.apply {
+ addModifiers(PUBLIC)
+ superclass(database.typeName)
+ addMethod(createCreateOpenHelper())
+ addMethod(createCreateInvalidationTracker())
+ addMethod(createClearAllTables())
+ }
+ addDaoImpls(builder)
+ return builder
+ }
+
+ private fun createClearAllTables(): MethodSpec {
+ val scope = CodeGenScope(this)
+ return MethodSpec.methodBuilder("clearAllTables").apply {
+ val dbVar = scope.getTmpVar("_db")
+ addStatement("final $T $L = super.getOpenHelper().getWritableDatabase()",
+ SupportDbTypeNames.DB, dbVar)
+ val deferVar = scope.getTmpVar("_supportsDeferForeignKeys")
+ if (database.enableForeignKeys) {
+ addStatement("boolean $L = $L.VERSION.SDK_INT >= $L.VERSION_CODES.LOLLIPOP",
+ deferVar, AndroidTypeNames.BUILD, AndroidTypeNames.BUILD)
+ }
+ addAnnotation(Override::class.java)
+ addModifiers(PUBLIC)
+ returns(TypeName.VOID)
+ beginControlFlow("try").apply {
+ if (database.enableForeignKeys) {
+ beginControlFlow("if (!$L)", deferVar).apply {
+ addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = FALSE")
+ }
+ endControlFlow()
+ }
+ addStatement("super.beginTransaction()")
+ if (database.enableForeignKeys) {
+ beginControlFlow("if ($L)", deferVar).apply {
+ addStatement("$L.execSQL($S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
+ }
+ endControlFlow()
+ }
+ database.entities.sortedWith(EntityDeleteComparator()).forEach {
+ addStatement("$L.execSQL($S)", dbVar, "DELETE FROM `${it.tableName}`")
+ }
+ addStatement("super.setTransactionSuccessful()")
+ }
+ nextControlFlow("finally").apply {
+ addStatement("super.endTransaction()")
+ if (database.enableForeignKeys) {
+ beginControlFlow("if (!$L)", deferVar).apply {
+ addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = TRUE")
+ }
+ endControlFlow()
+ }
+ addStatement("$L.query($S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
+ addStatement("$L.execSQL($S)", dbVar, "VACUUM")
+ }
+ endControlFlow()
+ }.build()
+ }
+
+ private fun createCreateInvalidationTracker(): MethodSpec {
+ return MethodSpec.methodBuilder("createInvalidationTracker").apply {
+ addAnnotation(Override::class.java)
+ addModifiers(PROTECTED)
+ returns(RoomTypeNames.INVALIDATION_TRACKER)
+ val tableNames = database.entities.joinToString(",") {
+ "\"${it.tableName}\""
+ }
+ addStatement("return new $T(this, $L)", RoomTypeNames.INVALIDATION_TRACKER, tableNames)
+ }.build()
+ }
+
+ private fun addDaoImpls(builder: TypeSpec.Builder) {
+ val scope = CodeGenScope(this)
+ builder.apply {
+ database.daoMethods.forEach { method ->
+ val name = method.dao.typeName.simpleName().decapitalize().stripNonJava()
+ val fieldName = scope.getTmpVar("_$name")
+ val field = FieldSpec.builder(method.dao.typeName, fieldName,
+ PRIVATE, VOLATILE).build()
+ addField(field)
+ addMethod(createDaoGetter(field, method))
+ }
+ }
+ }
+
+ private fun createDaoGetter(field: FieldSpec, method: DaoMethod): MethodSpec {
+ return MethodSpec.overriding(MoreElements.asExecutable(method.element)).apply {
+ beginControlFlow("if ($N != null)", field).apply {
+ addStatement("return $N", field)
+ }
+ nextControlFlow("else").apply {
+ beginControlFlow("synchronized(this)").apply {
+ beginControlFlow("if($N == null)", field).apply {
+ addStatement("$N = new $T(this)", field, method.dao.implTypeName)
+ }
+ endControlFlow()
+ addStatement("return $N", field)
+ }
+ endControlFlow()
+ }
+ endControlFlow()
+ }.build()
+ }
+
+ private fun createCreateOpenHelper(): MethodSpec {
+ val scope = CodeGenScope(this)
+ return MethodSpec.methodBuilder("createOpenHelper").apply {
+ addModifiers(Modifier.PROTECTED)
+ addAnnotation(Override::class.java)
+ returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)
+
+ val configParam = ParameterSpec.builder(RoomTypeNames.ROOM_DB_CONFIG,
+ "configuration").build()
+ addParameter(configParam)
+
+ val openHelperVar = scope.getTmpVar("_helper")
+ val openHelperCode = scope.fork()
+ SQLiteOpenHelperWriter(database)
+ .write(openHelperVar, configParam, openHelperCode)
+ addCode(openHelperCode.builder().build())
+ addStatement("return $L", openHelperVar)
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/EntityCursorConverterWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/EntityCursorConverterWriter.kt
new file mode 100644
index 0000000..69c8ec4
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/EntityCursorConverterWriter.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Entity
+import androidx.room.vo.FieldWithIndex
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeName
+import stripNonJava
+import javax.lang.model.element.Modifier.PRIVATE
+
+class EntityCursorConverterWriter(val entity: Entity) : ClassWriter.SharedMethodSpec(
+ "entityCursorConverter_${entity.typeName.toString().stripNonJava()}") {
+ override fun getUniqueKey(): String {
+ return "generic_entity_converter_of_${entity.element.qualifiedName}"
+ }
+
+ override fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder) {
+ builder.apply {
+ val cursorParam = ParameterSpec
+ .builder(AndroidTypeNames.CURSOR, "cursor").build()
+ addParameter(cursorParam)
+ addModifiers(PRIVATE)
+ returns(entity.typeName)
+ addCode(buildConvertMethodBody(writer, cursorParam))
+ }
+ }
+
+ private fun depth(parent: EmbeddedField?): Int {
+ return if (parent == null) {
+ 0
+ } else {
+ 1 + depth(parent.parent)
+ }
+ }
+
+ private fun buildConvertMethodBody(writer: ClassWriter, cursorParam: ParameterSpec): CodeBlock {
+ val scope = CodeGenScope(writer)
+ val entityVar = scope.getTmpVar("_entity")
+ scope.builder().apply {
+ scope.builder().addStatement("final $T $L", entity.typeName, entityVar)
+ val fieldsWithIndices = entity.fields.map {
+ val indexVar = scope.getTmpVar(
+ "_cursorIndexOf${it.name.stripNonJava().capitalize()}")
+ scope.builder().addStatement("final $T $L = $N.getColumnIndex($S)",
+ TypeName.INT, indexVar, cursorParam, it.columnName)
+ FieldWithIndex(field = it,
+ indexVar = indexVar,
+ alwaysExists = false)
+ }
+ FieldReadWriteWriter.readFromCursor(
+ outVar = entityVar,
+ outPojo = entity,
+ cursorVar = cursorParam.name,
+ fieldsWithIndices = fieldsWithIndices,
+ relationCollectors = emptyList(), // no relationship for entities
+ scope = scope)
+ addStatement("return $L", entityVar)
+ }
+ return scope.builder().build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/EntityDeleteComparator.kt b/room/compiler/src/main/kotlin/androidx/room/writer/EntityDeleteComparator.kt
new file mode 100644
index 0000000..71828b4
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/EntityDeleteComparator.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 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 androidx.room.writer
+
+import androidx.room.vo.Entity
+
+/**
+ * Sorts the entities by their foreign key dependencies. For example, when Entity A depends on
+ * Entity B, A is ordered before B.
+ */
+class EntityDeleteComparator : Comparator<Entity> {
+
+ override fun compare(lhs: Entity, rhs: Entity): Int {
+ val ltr = lhs.shouldBeDeletedAfter(rhs)
+ val rtl = rhs.shouldBeDeletedAfter(lhs)
+ return when {
+ ltr == rtl -> 0
+ ltr -> -1
+ rtl -> 1
+ else -> 0 // Never happens
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
new file mode 100644
index 0000000..553bcad
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Entity
+import androidx.room.vo.FieldWithIndex
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier.PUBLIC
+
+class EntityDeletionAdapterWriter(val entity: Entity) {
+ fun createAnonymous(classWriter: ClassWriter, dbParam: String): TypeSpec {
+ @Suppress("RemoveSingleExpressionStringTemplate")
+ return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
+ superclass(ParameterizedTypeName.get(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER,
+ entity.typeName)
+ )
+ addMethod(MethodSpec.methodBuilder("createQuery").apply {
+ addAnnotation(Override::class.java)
+ returns(ClassName.get("java.lang", "String"))
+ addModifiers(PUBLIC)
+ val query = "DELETE FROM `${entity.tableName}` WHERE " +
+ entity.primaryKey.fields.joinToString(" AND ") {
+ "`${it.columnName}` = ?"
+ }
+ addStatement("return $S", query)
+ }.build())
+ addMethod(MethodSpec.methodBuilder("bind").apply {
+ val bindScope = CodeGenScope(classWriter)
+ addAnnotation(Override::class.java)
+ val stmtParam = "stmt"
+ addParameter(ParameterSpec.builder(SupportDbTypeNames.SQLITE_STMT,
+ stmtParam).build())
+ val valueParam = "value"
+ addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
+ returns(TypeName.VOID)
+ addModifiers(PUBLIC)
+ val mapped = FieldWithIndex.byOrder(entity.primaryKey.fields)
+ FieldReadWriteWriter.bindToStatement(ownerVar = valueParam,
+ stmtParamVar = stmtParam,
+ fieldsWithIndices = mapped,
+ scope = bindScope)
+ addCode(bindScope.builder().build())
+ }.build())
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/EntityInsertionAdapterWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/EntityInsertionAdapterWriter.kt
new file mode 100644
index 0000000..0bd56b4
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/EntityInsertionAdapterWriter.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Entity
+import androidx.room.vo.FieldWithIndex
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier.PUBLIC
+
+class EntityInsertionAdapterWriter(val entity: Entity, val onConflict: String) {
+ fun createAnonymous(classWriter: ClassWriter, dbParam: String): TypeSpec {
+ @Suppress("RemoveSingleExpressionStringTemplate")
+ return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
+ superclass(
+ ParameterizedTypeName.get(RoomTypeNames.INSERTION_ADAPTER, entity.typeName)
+ )
+
+ // If there is an auto-increment primary key with primitive type, we consider 0 as
+ // not set. For such fields, we must generate a slightly different insertion SQL.
+ val primitiveAutoGenerateField = if (entity.primaryKey.autoGenerateId) {
+ entity.primaryKey.fields.firstOrNull()?.let { field ->
+ field.statementBinder?.typeMirror()?.let { binderType ->
+ if (binderType.kind.isPrimitive) {
+ field
+ } else {
+ null
+ }
+ }
+ }
+ } else {
+ null
+ }
+ addMethod(MethodSpec.methodBuilder("createQuery").apply {
+ addAnnotation(Override::class.java)
+ returns(ClassName.get("java.lang", "String"))
+ addModifiers(PUBLIC)
+ val query =
+ "INSERT OR $onConflict INTO `${entity.tableName}`(" +
+ entity.fields.joinToString(",") {
+ "`${it.columnName}`"
+ } + ") VALUES (" +
+ entity.fields.joinToString(",") {
+ if (primitiveAutoGenerateField == it) {
+ "nullif(?, 0)"
+ } else {
+ "?"
+ }
+ } + ")"
+ addStatement("return $S", query)
+ }.build())
+ addMethod(MethodSpec.methodBuilder("bind").apply {
+ val bindScope = CodeGenScope(classWriter)
+ addAnnotation(Override::class.java)
+ val stmtParam = "stmt"
+ addParameter(ParameterSpec.builder(SupportDbTypeNames.SQLITE_STMT,
+ stmtParam).build())
+ val valueParam = "value"
+ addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
+ returns(TypeName.VOID)
+ addModifiers(PUBLIC)
+ val mapped = FieldWithIndex.byOrder(entity.fields)
+ FieldReadWriteWriter.bindToStatement(
+ ownerVar = valueParam,
+ stmtParamVar = stmtParam,
+ fieldsWithIndices = mapped,
+ scope = bindScope
+ )
+ addCode(bindScope.builder().build())
+ }.build())
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
new file mode 100644
index 0000000..3363177
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
@@ -0,0 +1,86 @@
+/*
+ * 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Entity
+import androidx.room.vo.FieldWithIndex
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier.PUBLIC
+
+class EntityUpdateAdapterWriter(val entity: Entity, val onConflict: String) {
+ fun createAnonymous(classWriter: ClassWriter, dbParam: String): TypeSpec {
+ @Suppress("RemoveSingleExpressionStringTemplate")
+ return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
+ superclass(ParameterizedTypeName.get(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER,
+ entity.typeName)
+ )
+ addMethod(MethodSpec.methodBuilder("createQuery").apply {
+ addAnnotation(Override::class.java)
+ returns(ClassName.get("java.lang", "String"))
+ addModifiers(PUBLIC)
+ val query = "UPDATE OR $onConflict `${entity.tableName}` SET " +
+ entity.fields.joinToString(",") { field ->
+ "`${field.columnName}` = ?"
+ } + " WHERE " + entity.primaryKey.fields.joinToString(" AND ") {
+ "`${it.columnName}` = ?"
+ }
+ addStatement("return $S", query)
+ }.build())
+ addMethod(MethodSpec.methodBuilder("bind").apply {
+ val bindScope = CodeGenScope(classWriter)
+ addAnnotation(Override::class.java)
+ val stmtParam = "stmt"
+ addParameter(ParameterSpec.builder(SupportDbTypeNames.SQLITE_STMT,
+ stmtParam).build())
+ val valueParam = "value"
+ addParameter(ParameterSpec.builder(entity.typeName, valueParam).build())
+ returns(TypeName.VOID)
+ addModifiers(PUBLIC)
+ val mappedField = FieldWithIndex.byOrder(entity.fields)
+ FieldReadWriteWriter.bindToStatement(
+ ownerVar = valueParam,
+ stmtParamVar = stmtParam,
+ fieldsWithIndices = mappedField,
+ scope = bindScope
+ )
+ val pkeyStart = entity.fields.size
+ val mappedPrimaryKeys = entity.primaryKey.fields.mapIndexed { index, field ->
+ FieldWithIndex(field = field,
+ indexVar = "${pkeyStart + index + 1}",
+ alwaysExists = true)
+ }
+ FieldReadWriteWriter.bindToStatement(
+ ownerVar = valueParam,
+ stmtParamVar = stmtParam,
+ fieldsWithIndices = mappedPrimaryKeys,
+ scope = bindScope
+ )
+ addCode(bindScope.builder().build())
+ }.build())
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
new file mode 100644
index 0000000..ba765b8
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
@@ -0,0 +1,378 @@
+/*
+ * 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.ext.defaultValue
+import androidx.room.ext.typeName
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.CallType
+import androidx.room.vo.Constructor
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Field
+import androidx.room.vo.FieldWithIndex
+import androidx.room.vo.Pojo
+import androidx.room.vo.RelationCollector
+import com.squareup.javapoet.TypeName
+
+/**
+ * Handles writing a field into statement or reading it form statement.
+ */
+class FieldReadWriteWriter(fieldWithIndex: FieldWithIndex) {
+ val field = fieldWithIndex.field
+ val indexVar = fieldWithIndex.indexVar
+ val alwaysExists = fieldWithIndex.alwaysExists
+
+ companion object {
+ /*
+ * Get all parents including the ones which have grand children in this list but does not
+ * have any direct children in the list.
+ */
+ fun getAllParents(fields: List<Field>): Set<EmbeddedField> {
+ val allParents = mutableSetOf<EmbeddedField>()
+ fun addAllParents(field: Field) {
+ var parent = field.parent
+ while (parent != null) {
+ if (allParents.add(parent)) {
+ parent = parent.parent
+ } else {
+ break
+ }
+ }
+ }
+ fields.forEach(::addAllParents)
+ return allParents
+ }
+
+ /**
+ * Convert the fields with indices into a Node tree so that we can recursively process
+ * them. This work is done here instead of parsing because the result may include arbitrary
+ * fields.
+ */
+ private fun createNodeTree(
+ rootVar: String,
+ fieldsWithIndices: List<FieldWithIndex>,
+ scope: CodeGenScope): Node {
+ val allParents = getAllParents(fieldsWithIndices.map { it.field })
+ val rootNode = Node(rootVar, null)
+ rootNode.directFields = fieldsWithIndices.filter { it.field.parent == null }
+ val parentNodes = allParents.associate {
+ Pair(it, Node(
+ varName = scope.getTmpVar("_tmp${it.field.name.capitalize()}"),
+ fieldParent = it))
+ }
+ parentNodes.values.forEach { node ->
+ val fieldParent = node.fieldParent!!
+ val grandParent = fieldParent.parent
+ val grandParentNode = grandParent?.let {
+ parentNodes[it]
+ } ?: rootNode
+ node.directFields = fieldsWithIndices.filter { it.field.parent == fieldParent }
+ node.parentNode = grandParentNode
+ grandParentNode.subNodes.add(node)
+ }
+ return rootNode
+ }
+
+ fun bindToStatement(
+ ownerVar: String,
+ stmtParamVar: String,
+ fieldsWithIndices: List<FieldWithIndex>,
+ scope: CodeGenScope
+ ) {
+ fun visitNode(node: Node) {
+ fun bindWithDescendants() {
+ node.directFields.forEach {
+ FieldReadWriteWriter(it).bindToStatement(
+ ownerVar = node.varName,
+ stmtParamVar = stmtParamVar,
+ scope = scope
+ )
+ }
+ node.subNodes.forEach(::visitNode)
+ }
+
+ val fieldParent = node.fieldParent
+ if (fieldParent != null) {
+ fieldParent.getter.writeGet(
+ ownerVar = node.parentNode!!.varName,
+ outVar = node.varName,
+ builder = scope.builder()
+ )
+ scope.builder().apply {
+ beginControlFlow("if($L != null)", node.varName).apply {
+ bindWithDescendants()
+ }
+ nextControlFlow("else").apply {
+ node.allFields().forEach {
+ addStatement("$L.bindNull($L)", stmtParamVar, it.indexVar)
+ }
+ }
+ endControlFlow()
+ }
+ } else {
+ bindWithDescendants()
+ }
+ }
+ visitNode(createNodeTree(ownerVar, fieldsWithIndices, scope))
+ }
+
+ /**
+ * Just constructs the given item, does NOT DECLARE. Declaration happens outside the
+ * reading statement since we may never read if the cursor does not have necessary
+ * columns.
+ */
+ private fun construct(
+ outVar: String,
+ constructor: Constructor?,
+ typeName: TypeName,
+ localVariableNames: Map<String, FieldWithIndex>,
+ localEmbeddeds: List<Node>, scope: CodeGenScope
+ ) {
+ if (constructor == null) {
+ // best hope code generation
+ scope.builder().apply {
+ addStatement("$L = new $T()", outVar, typeName)
+ }
+ return
+ }
+ val variableNames = constructor.params.map { param ->
+ when (param) {
+ is Constructor.FieldParam -> localVariableNames.entries.firstOrNull {
+ it.value.field === param.field
+ }?.key
+ is Constructor.EmbeddedParam -> localEmbeddeds.firstOrNull {
+ it.fieldParent === param.embedded
+ }?.varName
+ else -> null
+ }
+ }
+ val args = variableNames.joinToString(",") { it ?: "null" }
+ scope.builder().apply {
+ addStatement("$L = new $T($L)", outVar, typeName, args)
+ }
+ }
+
+ /**
+ * Reads the row into the given variable. It does not declare it but constructs it.
+ */
+ fun readFromCursor(
+ outVar: String,
+ outPojo: Pojo,
+ cursorVar: String,
+ fieldsWithIndices: List<FieldWithIndex>,
+ scope: CodeGenScope,
+ relationCollectors: List<RelationCollector>
+ ) {
+ fun visitNode(node: Node) {
+ val fieldParent = node.fieldParent
+ fun readNode() {
+ // read constructor parameters into local fields
+ val constructorFields = node.directFields.filter {
+ it.field.setter.callType == CallType.CONSTRUCTOR
+ }.associateBy { fwi ->
+ FieldReadWriteWriter(fwi).readIntoTmpVar(cursorVar, scope)
+ }
+ // read decomposed fields
+ node.subNodes.forEach(::visitNode)
+ // construct the object
+ if (fieldParent != null) {
+ construct(outVar = node.varName,
+ constructor = fieldParent.pojo.constructor,
+ typeName = fieldParent.field.typeName,
+ localEmbeddeds = node.subNodes,
+ localVariableNames = constructorFields,
+ scope = scope)
+ } else {
+ construct(outVar = node.varName,
+ constructor = outPojo.constructor,
+ typeName = outPojo.typeName,
+ localEmbeddeds = node.subNodes,
+ localVariableNames = constructorFields,
+ scope = scope)
+ }
+ // ready any field that was not part of the constructor
+ node.directFields.filterNot {
+ it.field.setter.callType == CallType.CONSTRUCTOR
+ }.forEach { fwi ->
+ FieldReadWriteWriter(fwi).readFromCursor(
+ ownerVar = node.varName,
+ cursorVar = cursorVar,
+ scope = scope)
+ }
+ // assign relationship fields which will be read later
+ relationCollectors.filter { (relation) ->
+ relation.field.parent === fieldParent
+ }.forEach {
+ it.writeReadParentKeyCode(
+ cursorVarName = cursorVar,
+ itemVar = node.varName,
+ fieldsWithIndices = fieldsWithIndices,
+ scope = scope)
+ }
+ // assign sub modes to fields if they were not part of the constructor.
+ node.subNodes.map {
+ val setter = it.fieldParent?.setter
+ if (setter != null && setter.callType != CallType.CONSTRUCTOR) {
+ Pair(it.varName, setter)
+ } else {
+ null
+ }
+ }.filterNotNull().forEach { (varName, setter) ->
+ setter.writeSet(
+ ownerVar = node.varName,
+ inVar = varName,
+ builder = scope.builder())
+ }
+ }
+ if (fieldParent == null) {
+ // root element
+ // always declared by the caller so we don't declare this
+ readNode()
+ } else {
+ // always declare, we'll set below
+ scope.builder().addStatement("final $T $L", fieldParent.pojo.typeName,
+ node.varName)
+ if (fieldParent.nonNull) {
+ readNode()
+ } else {
+ val myDescendants = node.allFields()
+ val allNullCheck = myDescendants.joinToString(" && ") {
+ if (it.alwaysExists) {
+ "$cursorVar.isNull(${it.indexVar})"
+ } else {
+ "( ${it.indexVar} == -1 || $cursorVar.isNull(${it.indexVar}))"
+ }
+ }
+ scope.builder().apply {
+ beginControlFlow("if (! ($L))", allNullCheck).apply {
+ readNode()
+ }
+ nextControlFlow(" else ").apply {
+ addStatement("$L = null", node.varName)
+ }
+ endControlFlow()
+ }
+ }
+ }
+ }
+ visitNode(createNodeTree(outVar, fieldsWithIndices, scope))
+ }
+ }
+
+ /**
+ * @param ownerVar The entity / pojo that owns this field. It must own this field! (not the
+ * container pojo)
+ * @param stmtParamVar The statement variable
+ * @param scope The code generation scope
+ */
+ private fun bindToStatement(ownerVar: String, stmtParamVar: String, scope: CodeGenScope) {
+ field.statementBinder?.let { binder ->
+ val varName = if (field.getter.callType == CallType.FIELD) {
+ "$ownerVar.${field.name}"
+ } else {
+ "$ownerVar.${field.getter.name}()"
+ }
+ binder.bindToStmt(stmtParamVar, indexVar, varName, scope)
+ }
+ }
+
+ /**
+ * @param ownerVar The entity / pojo that owns this field. It must own this field (not the
+ * container pojo)
+ * @param cursorVar The cursor variable
+ * @param scope The code generation scope
+ */
+ private fun readFromCursor(ownerVar: String, cursorVar: String, scope: CodeGenScope) {
+ fun toRead() {
+ field.cursorValueReader?.let { reader ->
+ scope.builder().apply {
+ when (field.setter.callType) {
+ CallType.FIELD -> {
+ reader.readFromCursor("$ownerVar.${field.getter.name}", cursorVar,
+ indexVar, scope)
+ }
+ CallType.METHOD -> {
+ val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
+ addStatement("final $T $L", field.getter.type.typeName(), tmpField)
+ reader.readFromCursor(tmpField, cursorVar, indexVar, scope)
+ addStatement("$L.$L($L)", ownerVar, field.setter.name, tmpField)
+ }
+ CallType.CONSTRUCTOR -> {
+ // no-op
+ }
+ }
+ }
+ }
+ }
+ if (alwaysExists) {
+ toRead()
+ } else {
+ scope.builder().apply {
+ beginControlFlow("if ($L != -1)", indexVar).apply {
+ toRead()
+ }
+ endControlFlow()
+ }
+ }
+ }
+
+ /**
+ * Reads the value into a temporary local variable.
+ */
+ fun readIntoTmpVar(cursorVar: String, scope: CodeGenScope): String {
+ val tmpField = scope.getTmpVar("_tmp${field.name.capitalize()}")
+ val typeName = field.getter.type.typeName()
+ scope.builder().apply {
+ addStatement("final $T $L", typeName, tmpField)
+ if (alwaysExists) {
+ field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
+ } else {
+ beginControlFlow("if ($L == -1)", indexVar).apply {
+ addStatement("$L = $L", tmpField, typeName.defaultValue())
+ }
+ nextControlFlow("else").apply {
+ field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
+ }
+ endControlFlow()
+ }
+ }
+ return tmpField
+ }
+
+ /**
+ * On demand node which is created based on the fields that were passed into this class.
+ */
+ private class Node(
+ // root for me
+ val varName: String,
+ // set if I'm a FieldParent
+ val fieldParent: EmbeddedField?
+ ) {
+ // whom do i belong
+ var parentNode: Node? = null
+ // these fields are my direct fields
+ lateinit var directFields: List<FieldWithIndex>
+ // these nodes are under me
+ val subNodes = mutableListOf<Node>()
+
+ fun allFields(): List<FieldWithIndex> {
+ return directFields + subNodes.flatMap { it.allFields() }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/PreparedStatementWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/PreparedStatementWriter.kt
new file mode 100644
index 0000000..e035f8f
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/PreparedStatementWriter.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+
+/**
+ * Creates anonymous classes for RoomTypeNames#SHARED_SQLITE_STMT.
+ */
+class PreparedStatementWriter(val queryWriter: QueryWriter) {
+ fun createAnonymous(classWriter: ClassWriter, dbParam: FieldSpec): TypeSpec {
+ val scope = CodeGenScope(classWriter)
+ @Suppress("RemoveSingleExpressionStringTemplate")
+ return TypeSpec.anonymousClassBuilder("$N", dbParam).apply {
+ superclass(RoomTypeNames.SHARED_SQLITE_STMT)
+ addMethod(MethodSpec.methodBuilder("createQuery").apply {
+ addAnnotation(Override::class.java)
+ returns(ClassName.get("java.lang", "String"))
+ addModifiers(Modifier.PUBLIC)
+ val queryName = scope.getTmpVar("_query")
+ val queryGenScope = scope.fork()
+ queryWriter.prepareQuery(queryName, queryGenScope)
+ addCode(queryGenScope.builder().build())
+ addStatement("return $L", queryName)
+ }.build())
+ }.build()
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt
new file mode 100644
index 0000000..3bf902a
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/QueryWriter.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.ext.L
+import androidx.room.ext.RoomTypeNames.ROOM_SQL_QUERY
+import androidx.room.ext.RoomTypeNames.STRING_UTIL
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.parser.ParsedQuery
+import androidx.room.parser.Section
+import androidx.room.parser.SectionType.BIND_VAR
+import androidx.room.parser.SectionType.NEWLINE
+import androidx.room.parser.SectionType.TEXT
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.QueryMethod
+import androidx.room.vo.QueryParameter
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+
+/**
+ * Writes the SQL query and arguments for a QueryMethod.
+ */
+class QueryWriter constructor(val parameters: List<QueryParameter>,
+ val sectionToParamMapping: List<Pair<Section, QueryParameter?>>,
+ val query: ParsedQuery) {
+
+ constructor(queryMethod: QueryMethod) : this(queryMethod.parameters,
+ queryMethod.sectionToParamMapping, queryMethod.query) {
+ }
+
+ fun prepareReadAndBind(outSqlQueryName: String, outRoomSQLiteQueryVar: String,
+ scope: CodeGenScope) {
+ val listSizeVars = createSqlQueryAndArgs(outSqlQueryName, outRoomSQLiteQueryVar, scope)
+ bindArgs(outRoomSQLiteQueryVar, listSizeVars, scope)
+ }
+
+ fun prepareQuery(
+ outSqlQueryName: String, scope: CodeGenScope): List<Pair<QueryParameter, String>> {
+ return createSqlQueryAndArgs(outSqlQueryName, null, scope)
+ }
+
+ private fun createSqlQueryAndArgs(
+ outSqlQueryName: String,
+ outArgsName: String?,
+ scope: CodeGenScope
+ ): List<Pair<QueryParameter, String>> {
+ val listSizeVars = arrayListOf<Pair<QueryParameter, String>>()
+ val varargParams = parameters
+ .filter { it.queryParamAdapter?.isMultiple ?: false }
+ val sectionToParamMapping = sectionToParamMapping
+ val knownQueryArgsCount = sectionToParamMapping.filterNot {
+ it.second?.queryParamAdapter?.isMultiple ?: false
+ }.size
+ scope.builder().apply {
+ if (varargParams.isNotEmpty()) {
+ val stringBuilderVar = scope.getTmpVar("_stringBuilder")
+ addStatement("$T $L = $T.newStringBuilder()",
+ ClassName.get(StringBuilder::class.java), stringBuilderVar, STRING_UTIL)
+ query.sections.forEach {
+ when (it.type) {
+ TEXT -> addStatement("$L.append($S)", stringBuilderVar, it.text)
+ NEWLINE -> addStatement("$L.append($S)", stringBuilderVar, "\n")
+ BIND_VAR -> {
+ // If it is null, will be reported as error before. We just try out
+ // best to generate as much code as possible.
+ sectionToParamMapping.firstOrNull { mapping ->
+ mapping.first == it
+ }?.let { pair ->
+ if (pair.second?.queryParamAdapter?.isMultiple ?: false) {
+ val tmpCount = scope.getTmpVar("_inputSize")
+ listSizeVars.add(Pair(pair.second!!, tmpCount))
+ pair.second
+ ?.queryParamAdapter
+ ?.getArgCount(pair.second!!.name, tmpCount, scope)
+ addStatement("$T.appendPlaceholders($L, $L)",
+ STRING_UTIL, stringBuilderVar, tmpCount)
+ } else {
+ addStatement("$L.append($S)", stringBuilderVar, "?")
+ }
+ }
+ }
+ }
+ }
+
+ addStatement("final $T $L = $L.toString()", String::class.typeName(),
+ outSqlQueryName, stringBuilderVar)
+ if (outArgsName != null) {
+ val argCount = scope.getTmpVar("_argCount")
+ addStatement("final $T $L = $L$L", TypeName.INT, argCount, knownQueryArgsCount,
+ listSizeVars.joinToString("") { " + ${it.second}" })
+ addStatement("final $T $L = $T.acquire($L, $L)",
+ ROOM_SQL_QUERY, outArgsName, ROOM_SQL_QUERY, outSqlQueryName,
+ argCount)
+ }
+ } else {
+ addStatement("final $T $L = $S", String::class.typeName(),
+ outSqlQueryName, query.queryWithReplacedBindParams)
+ if (outArgsName != null) {
+ addStatement("final $T $L = $T.acquire($L, $L)",
+ ROOM_SQL_QUERY, outArgsName, ROOM_SQL_QUERY, outSqlQueryName,
+ knownQueryArgsCount)
+ }
+ }
+ }
+ return listSizeVars
+ }
+
+ fun bindArgs(
+ outArgsName: String,
+ listSizeVars: List<Pair<QueryParameter, String>>,
+ scope: CodeGenScope
+ ) {
+ if (parameters.isEmpty()) {
+ return
+ }
+ scope.builder().apply {
+ val argIndex = scope.getTmpVar("_argIndex")
+ addStatement("$T $L = $L", TypeName.INT, argIndex, 1)
+ // # of bindings with 1 placeholder
+ var constInputs = 0
+ // variable names for size of the bindings that have multiple args
+ val varInputs = arrayListOf<String>()
+ sectionToParamMapping.forEach { pair ->
+ // reset the argIndex to the correct start index
+ if (constInputs > 0 || varInputs.isNotEmpty()) {
+ addStatement("$L = $L$L", argIndex,
+ if (constInputs > 0) (1 + constInputs) else "1",
+ varInputs.joinToString("") { " + $it" })
+ }
+ val param = pair.second
+ param?.let {
+ param.queryParamAdapter?.bindToStmt(param.name, outArgsName, argIndex, scope)
+ }
+ // add these to the list so that we can use them to calculate the next count.
+ val sizeVar = listSizeVars.firstOrNull { it.first == param }
+ if (sizeVar == null) {
+ constInputs ++
+ } else {
+ varInputs.add(sizeVar.second)
+ }
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
new file mode 100644
index 0000000..b8b1ca1
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/RelationCollectorMethodWriter.kt
@@ -0,0 +1,162 @@
+/*
+ * 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 androidx.room.writer
+
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.RelationCollector
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import stripNonJava
+import javax.lang.model.element.Modifier
+
+/**
+ * Writes the method that fetches the relations of a POJO and assigns them into the given map.
+ */
+class RelationCollectorMethodWriter(private val collector: RelationCollector)
+ : ClassWriter.SharedMethodSpec(
+ "fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
+ "As${collector.relation.pojoTypeName.toString().stripNonJava()}") {
+ companion object {
+ val KEY_SET_VARIABLE = "__mapKeySet"
+ }
+ override fun getUniqueKey(): String {
+ val relation = collector.relation
+ return "RelationCollectorMethodWriter" +
+ "-${collector.mapTypeName}" +
+ "-${relation.entity.typeName}" +
+ "-${relation.entityField.columnName}" +
+ "-${relation.pojoTypeName}" +
+ "-${relation.createLoadAllSql()}"
+ }
+
+ override fun prepare(methodName: String, writer: ClassWriter, builder: MethodSpec.Builder) {
+ val scope = CodeGenScope(writer)
+ val relation = collector.relation
+
+ val param = ParameterSpec.builder(collector.mapTypeName, "_map")
+ .addModifiers(Modifier.FINAL)
+ .build()
+ val sqlQueryVar = scope.getTmpVar("_sql")
+ val keySetVar = KEY_SET_VARIABLE
+
+ val cursorVar = "_cursor"
+ val itemKeyIndexVar = "_itemKeyIndex"
+ val stmtVar = scope.getTmpVar("_stmt")
+ scope.builder().apply {
+
+ val keySetType = ParameterizedTypeName.get(
+ ClassName.get(Set::class.java), collector.keyTypeName
+ )
+ addStatement("final $T $L = $N.keySet()", keySetType, keySetVar, param)
+ beginControlFlow("if ($L.isEmpty())", keySetVar).apply {
+ addStatement("return")
+ }
+ endControlFlow()
+ addStatement("// check if the size is too big, if so divide")
+ beginControlFlow("if($N.size() > $T.MAX_BIND_PARAMETER_CNT)",
+ param, RoomTypeNames.ROOM_DB).apply {
+ // divide it into chunks
+ val tmpMapVar = scope.getTmpVar("_tmpInnerMap")
+ addStatement("$T $L = new $T($L.MAX_BIND_PARAMETER_CNT)",
+ collector.mapTypeName, tmpMapVar,
+ collector.mapTypeName, RoomTypeNames.ROOM_DB)
+ val mapIndexVar = scope.getTmpVar("_mapIndex")
+ val tmpIndexVar = scope.getTmpVar("_tmpIndex")
+ val limitVar = scope.getTmpVar("_limit")
+ addStatement("$T $L = 0", TypeName.INT, mapIndexVar)
+ addStatement("$T $L = 0", TypeName.INT, tmpIndexVar)
+ addStatement("final $T $L = $N.size()", TypeName.INT, limitVar, param)
+ beginControlFlow("while($L < $L)", mapIndexVar, limitVar).apply {
+ addStatement("$L.put($N.keyAt($L), $N.valueAt($L))",
+ tmpMapVar, param, mapIndexVar, param, mapIndexVar)
+ addStatement("$L++", mapIndexVar)
+ addStatement("$L++", tmpIndexVar)
+ beginControlFlow("if($L == $T.MAX_BIND_PARAMETER_CNT)",
+ tmpIndexVar, RoomTypeNames.ROOM_DB).apply {
+ // recursively load that batch
+ addStatement("$L($L)", methodName, tmpMapVar)
+ // clear nukes the backing data hence we create a new one
+ addStatement("$L = new $T($T.MAX_BIND_PARAMETER_CNT)",
+ tmpMapVar, collector.mapTypeName, RoomTypeNames.ROOM_DB)
+ addStatement("$L = 0", tmpIndexVar)
+ }.endControlFlow()
+ }.endControlFlow()
+ beginControlFlow("if($L > 0)", tmpIndexVar).apply {
+ // load the last batch
+ addStatement("$L($L)", methodName, tmpMapVar)
+ }.endControlFlow()
+ addStatement("return")
+ }.endControlFlow()
+ collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
+
+ addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
+ DaoWriter.dbField, stmtVar)
+
+ beginControlFlow("try").apply {
+ addStatement("final $T $L = $L.getColumnIndex($S)",
+ TypeName.INT, itemKeyIndexVar, cursorVar, relation.entityField.columnName)
+
+ beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
+ addStatement("return")
+ }
+ endControlFlow()
+
+ collector.rowAdapter.onCursorReady(cursorVar, scope)
+ val tmpVarName = scope.getTmpVar("_item")
+ beginControlFlow("while($L.moveToNext())", cursorVar).apply {
+ // read key from the cursor
+ collector.readKey(
+ cursorVarName = cursorVar,
+ indexVar = itemKeyIndexVar,
+ scope = scope
+ ) { keyVar ->
+ val collectionVar = scope.getTmpVar("_tmpCollection")
+ addStatement("$T $L = $N.get($L)", collector.collectionTypeName,
+ collectionVar, param, keyVar)
+ beginControlFlow("if ($L != null)", collectionVar).apply {
+ addStatement("final $T $L", relation.pojoTypeName, tmpVarName)
+ collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
+ addStatement("$L.add($L)", collectionVar, tmpVarName)
+ }
+ endControlFlow()
+ }
+ }
+ endControlFlow()
+ collector.rowAdapter.onCursorFinished()?.invoke(scope)
+ }
+ nextControlFlow("finally").apply {
+ addStatement("$L.close()", cursorVar)
+ }
+ endControlFlow()
+ }
+ builder.apply {
+ addModifiers(Modifier.PRIVATE)
+ addParameter(param)
+ returns(TypeName.VOID)
+ addCode(scope.builder().build())
+ }
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
new file mode 100644
index 0000000..17501dc
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.annotation.VisibleForTesting
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Database
+import androidx.room.vo.Entity
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier.PROTECTED
+import javax.lang.model.element.Modifier.PUBLIC
+
+/**
+ * Create an open helper using SupportSQLiteOpenHelperFactory
+ */
+class SQLiteOpenHelperWriter(val database: Database) {
+ fun write(outVar: String, configuration: ParameterSpec, scope: CodeGenScope) {
+ scope.builder().apply {
+ val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
+ val callbackVar = scope.getTmpVar("_openCallback")
+ addStatement("final $T $L = new $T($N, $L, $S, $S)",
+ SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
+ callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
+ createOpenCallback(scope), database.identityHash, database.legacyIdentityHash)
+ // build configuration
+ addStatement(
+ """
+ final $T $L = $T.builder($N.context)
+ .name($N.name)
+ .callback($L)
+ .build()
+ """.trimIndent(),
+ SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG, sqliteConfigVar,
+ SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
+ configuration, configuration, callbackVar)
+ addStatement("final $T $N = $N.sqliteOpenHelperFactory.create($L)",
+ SupportDbTypeNames.SQLITE_OPEN_HELPER, outVar,
+ configuration, sqliteConfigVar)
+ }
+ }
+
+ private fun createOpenCallback(scope: CodeGenScope): TypeSpec {
+ return TypeSpec.anonymousClassBuilder(L, database.version).apply {
+ superclass(RoomTypeNames.OPEN_HELPER_DELEGATE)
+ addMethod(createCreateAllTables())
+ addMethod(createDropAllTables())
+ addMethod(createOnCreate(scope.fork()))
+ addMethod(createOnOpen(scope.fork()))
+ addMethod(createValidateMigration(scope.fork()))
+ }.build()
+ }
+
+ private fun createValidateMigration(scope: CodeGenScope): MethodSpec {
+ return MethodSpec.methodBuilder("validateMigration").apply {
+ addModifiers(PROTECTED)
+ addAnnotation(Override::class.java)
+ val dbParam = ParameterSpec.builder(SupportDbTypeNames.DB, "_db").build()
+ addParameter(dbParam)
+ database.entities.forEach { entity ->
+ val methodScope = scope.fork()
+ TableInfoValidationWriter(entity).write(dbParam, methodScope)
+ addCode(methodScope.builder().build())
+ }
+ }.build()
+ }
+
+ private fun createOnCreate(scope: CodeGenScope): MethodSpec {
+ return MethodSpec.methodBuilder("onCreate").apply {
+ addModifiers(PROTECTED)
+ addAnnotation(Override::class.java)
+ addParameter(SupportDbTypeNames.DB, "_db")
+ invokeCallbacks(scope, "onCreate")
+ }.build()
+ }
+
+ private fun createOnOpen(scope: CodeGenScope): MethodSpec {
+ return MethodSpec.methodBuilder("onOpen").apply {
+ addModifiers(PUBLIC)
+ addAnnotation(Override::class.java)
+ addParameter(SupportDbTypeNames.DB, "_db")
+ addStatement("mDatabase = _db")
+ if (database.enableForeignKeys) {
+ addStatement("_db.execSQL($S)", "PRAGMA foreign_keys = ON")
+ }
+ addStatement("internalInitInvalidationTracker(_db)")
+ invokeCallbacks(scope, "onOpen")
+ }.build()
+ }
+
+ private fun createCreateAllTables(): MethodSpec {
+ return MethodSpec.methodBuilder("createAllTables").apply {
+ addModifiers(PUBLIC)
+ addAnnotation(Override::class.java)
+ addParameter(SupportDbTypeNames.DB, "_db")
+ database.bundle.buildCreateQueries().forEach {
+ addStatement("_db.execSQL($S)", it)
+ }
+ }.build()
+ }
+
+ private fun createDropAllTables(): MethodSpec {
+ return MethodSpec.methodBuilder("dropAllTables").apply {
+ addModifiers(PUBLIC)
+ addAnnotation(Override::class.java)
+ addParameter(SupportDbTypeNames.DB, "_db")
+ database.entities.forEach {
+ addStatement("_db.execSQL($S)", createDropTableQuery(it))
+ }
+ }.build()
+ }
+
+ private fun MethodSpec.Builder.invokeCallbacks(scope: CodeGenScope, methodName: String) {
+ val iVar = scope.getTmpVar("_i")
+ val sizeVar = scope.getTmpVar("_size")
+ beginControlFlow("if (mCallbacks != null)").apply {
+ beginControlFlow("for (int $N = 0, $N = mCallbacks.size(); $N < $N; $N++)",
+ iVar, sizeVar, iVar, sizeVar, iVar).apply {
+ addStatement("mCallbacks.get($N).$N(_db)", iVar, methodName)
+ }
+ endControlFlow()
+ }
+ endControlFlow()
+ }
+
+ @VisibleForTesting
+ fun createQuery(entity: Entity): String {
+ return entity.createTableQuery
+ }
+
+ @VisibleForTesting
+ fun createDropTableQuery(entity: Entity): String {
+ return "DROP TABLE IF EXISTS `${entity.tableName}`"
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
new file mode 100644
index 0000000..1b9964a
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
@@ -0,0 +1,114 @@
+/*
+ * 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 androidx.room.writer
+
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.solver.CodeGenScope
+import androidx.room.vo.Entity
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import stripNonJava
+import java.util.Arrays
+import java.util.HashMap
+import java.util.HashSet
+
+class TableInfoValidationWriter(val entity: Entity) {
+ fun write(dbParam: ParameterSpec, scope: CodeGenScope) {
+ val suffix = entity.tableName.stripNonJava().capitalize()
+ val expectedInfoVar = scope.getTmpVar("_info$suffix")
+ scope.builder().apply {
+ val columnListVar = scope.getTmpVar("_columns$suffix")
+ val columnListType = ParameterizedTypeName.get(HashMap::class.typeName(),
+ CommonTypeNames.STRING, RoomTypeNames.TABLE_INFO_COLUMN)
+
+ addStatement("final $T $L = new $T($L)", columnListType, columnListVar,
+ columnListType, entity.fields.size)
+ entity.fields.forEach { field ->
+ addStatement("$L.put($S, new $T($S, $S, $L, $L))",
+ columnListVar, field.columnName, RoomTypeNames.TABLE_INFO_COLUMN,
+ /*name*/ field.columnName,
+ /*type*/ field.affinity?.name ?: SQLTypeAffinity.TEXT.name,
+ /*nonNull*/ field.nonNull,
+ /*pkeyPos*/ entity.primaryKey.fields.indexOf(field) + 1)
+ }
+
+ val foreignKeySetVar = scope.getTmpVar("_foreignKeys$suffix")
+ val foreignKeySetType = ParameterizedTypeName.get(HashSet::class.typeName(),
+ RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
+ addStatement("final $T $L = new $T($L)", foreignKeySetType, foreignKeySetVar,
+ foreignKeySetType, entity.foreignKeys.size)
+ entity.foreignKeys.forEach {
+ val myColumnNames = it.childFields
+ .joinToString(",") { "\"${it.columnName}\"" }
+ val refColumnNames = it.parentColumns
+ .joinToString(",") { "\"$it\"" }
+ addStatement("$L.add(new $T($S, $S, $S," +
+ "$T.asList($L), $T.asList($L)))", foreignKeySetVar,
+ RoomTypeNames.TABLE_INFO_FOREIGN_KEY,
+ /*parent table*/ it.parentTable,
+ /*on delete*/ it.onDelete.sqlName,
+ /*on update*/ it.onUpdate.sqlName,
+ Arrays::class.typeName(),
+ /*parent names*/ myColumnNames,
+ Arrays::class.typeName(),
+ /*parent column names*/ refColumnNames)
+ }
+
+ val indicesSetVar = scope.getTmpVar("_indices$suffix")
+ val indicesType = ParameterizedTypeName.get(HashSet::class.typeName(),
+ RoomTypeNames.TABLE_INFO_INDEX)
+ addStatement("final $T $L = new $T($L)", indicesType, indicesSetVar,
+ indicesType, entity.indices.size)
+ entity.indices.forEach { index ->
+ val columnNames = index.fields
+ .joinToString(",") { "\"${it.columnName}\"" }
+ addStatement("$L.add(new $T($S, $L, $T.asList($L)))",
+ indicesSetVar,
+ RoomTypeNames.TABLE_INFO_INDEX,
+ index.name,
+ index.unique,
+ Arrays::class.typeName(),
+ columnNames)
+ }
+
+ addStatement("final $T $L = new $T($S, $L, $L, $L)",
+ RoomTypeNames.TABLE_INFO, expectedInfoVar, RoomTypeNames.TABLE_INFO,
+ entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar)
+
+ val existingVar = scope.getTmpVar("_existing$suffix")
+ addStatement("final $T $L = $T.read($N, $S)",
+ RoomTypeNames.TABLE_INFO, existingVar, RoomTypeNames.TABLE_INFO,
+ dbParam, entity.tableName)
+
+ beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
+ addStatement("throw new $T($S + $L + $S + $L)",
+ IllegalStateException::class.typeName(),
+ "Migration didn't properly handle ${entity.tableName}" +
+ "(${entity.element.qualifiedName}).\n Expected:\n",
+ expectedInfoVar, "\n Found:\n", existingVar)
+ }
+ endControlFlow()
+ }
+ }
+}
diff --git a/room/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/room/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor
index f628f64..ee7c7ee 100644
--- a/room/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor
+++ b/room/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -1 +1 @@
-android.arch.persistence.room.RoomProcessor
+androidx.room.RoomProcessor
diff --git a/room/compiler/src/test/data/common/input/Book.java b/room/compiler/src/test/data/common/input/Book.java
index 00cf8b6..bbad33d 100644
--- a/room/compiler/src/test/data/common/input/Book.java
+++ b/room/compiler/src/test/data/common/input/Book.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
@Entity
public class Book {
@PrimaryKey
diff --git a/room/compiler/src/test/data/common/input/ComputableLiveData.java b/room/compiler/src/test/data/common/input/ComputableLiveData.java
index f135244..da5a15e 100644
--- a/room/compiler/src/test/data/common/input/ComputableLiveData.java
+++ b/room/compiler/src/test/data/common/input/ComputableLiveData.java
@@ -1,6 +1,6 @@
//ComputableLiveData interface for tests
-package android.arch.lifecycle;
-import android.arch.lifecycle.LiveData;
+package androidx.lifecycle;
+import androidx.lifecycle.LiveData;
public abstract class ComputableLiveData<T> {
public ComputableLiveData(){}
abstract protected T compute();
diff --git a/room/compiler/src/test/data/common/input/DataSource.java b/room/compiler/src/test/data/common/input/DataSource.java
index 9f51539..157750a 100644
--- a/room/compiler/src/test/data/common/input/DataSource.java
+++ b/room/compiler/src/test/data/common/input/DataSource.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.arch.paging;
+package androidx.paging;
abstract public class DataSource<K, T> {
public interface Factory<Key, Value> {
diff --git a/room/compiler/src/test/data/common/input/LiveData.java b/room/compiler/src/test/data/common/input/LiveData.java
index 3aea6ac..c961d1c 100644
--- a/room/compiler/src/test/data/common/input/LiveData.java
+++ b/room/compiler/src/test/data/common/input/LiveData.java
@@ -1,4 +1,4 @@
//LiveData interface for tests
-package android.arch.lifecycle;
+package androidx.lifecycle;
public class LiveData<T> {
}
diff --git a/room/compiler/src/test/data/common/input/MultiPKeyEntity.java b/room/compiler/src/test/data/common/input/MultiPKeyEntity.java
index 59570ad..67c3bbf 100644
--- a/room/compiler/src/test/data/common/input/MultiPKeyEntity.java
+++ b/room/compiler/src/test/data/common/input/MultiPKeyEntity.java
@@ -15,8 +15,8 @@
*/
package foo.bar;
-import android.support.annotation.NonNull;
-import android.arch.persistence.room.*;
+import androidx.annotation.NonNull;
+import androidx.room.*;
@Entity(primaryKeys = {"name", "lastName"})
public class MultiPKeyEntity {
@NonNull
diff --git a/room/compiler/src/test/data/common/input/NotAnEntity.java b/room/compiler/src/test/data/common/input/NotAnEntity.java
index 90c1dd4..d8b4749 100644
--- a/room/compiler/src/test/data/common/input/NotAnEntity.java
+++ b/room/compiler/src/test/data/common/input/NotAnEntity.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
public class NotAnEntity {
int bookId;
int uid;
diff --git a/room/compiler/src/test/data/common/input/PositionalDataSource.java b/room/compiler/src/test/data/common/input/PositionalDataSource.java
index 7cff0d7..ff5338a 100644
--- a/room/compiler/src/test/data/common/input/PositionalDataSource.java
+++ b/room/compiler/src/test/data/common/input/PositionalDataSource.java
@@ -1,4 +1,4 @@
-package android.arch.paging;
+package androidx.paging;
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
diff --git a/room/compiler/src/test/data/common/input/Rx2Room.java b/room/compiler/src/test/data/common/input/Rx2Room.java
index da474fd..47b9553 100644
--- a/room/compiler/src/test/data/common/input/Rx2Room.java
+++ b/room/compiler/src/test/data/common/input/Rx2Room.java
@@ -1,5 +1,5 @@
// mock rx2 helper
-package android.arch.persistence.room;
+package androidx.room;
class RxRoom {
}
diff --git a/room/compiler/src/test/data/common/input/User.java b/room/compiler/src/test/data/common/input/User.java
index ce487d3..a8b7feb 100644
--- a/room/compiler/src/test/data/common/input/User.java
+++ b/room/compiler/src/test/data/common/input/User.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
@Entity
public class User {
@PrimaryKey
diff --git a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
index 89859e5..037261e 100644
--- a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
@@ -15,9 +15,9 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
import java.util.List;
-import android.arch.lifecycle.LiveData;
+import androidx.lifecycle.LiveData;
@Dao
abstract class ComplexDao {
static class FullName {
diff --git a/room/compiler/src/test/data/daoWriter/input/DeletionDao.java b/room/compiler/src/test/data/daoWriter/input/DeletionDao.java
index 997f290..a114c30 100644
--- a/room/compiler/src/test/data/daoWriter/input/DeletionDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/DeletionDao.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
import java.util.List;
@Dao
diff --git a/room/compiler/src/test/data/daoWriter/input/UpdateDao.java b/room/compiler/src/test/data/daoWriter/input/UpdateDao.java
index 8041268..def944b 100644
--- a/room/compiler/src/test/data/daoWriter/input/UpdateDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/UpdateDao.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
import java.util.List;
@Dao
diff --git a/room/compiler/src/test/data/daoWriter/input/WriterDao.java b/room/compiler/src/test/data/daoWriter/input/WriterDao.java
index e122479..6a1ebee 100644
--- a/room/compiler/src/test/data/daoWriter/input/WriterDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/WriterDao.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
import java.util.List;
@Dao
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index 7212da2..97878c6 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -1,13 +1,13 @@
package foo.bar;
-import android.arch.lifecycle.ComputableLiveData;
-import android.arch.lifecycle.LiveData;
-import android.arch.persistence.room.InvalidationTracker.Observer;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.RoomSQLiteQuery;
-import android.arch.persistence.room.util.StringUtil;
import android.database.Cursor;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ComputableLiveData;
+import androidx.lifecycle.LiveData;
+import androidx.room.InvalidationTracker.Observer;
+import androidx.room.RoomDatabase;
+import androidx.room.RoomSQLiteQuery;
+import androidx.room.util.StringUtil;
import java.lang.Integer;
import java.lang.Override;
import java.lang.String;
@@ -18,7 +18,7 @@
import java.util.Set;
import javax.annotation.Generated;
-@Generated("android.arch.persistence.room.RoomProcessor")
+@Generated("androidx.room.RoomProcessor")
@SuppressWarnings("unchecked")
public class ComplexDao_Impl extends ComplexDao {
private final RoomDatabase __db;
diff --git a/room/compiler/src/test/data/daoWriter/output/DeletionDao.java b/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
index 78ac94a..db9fb35 100644
--- a/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
@@ -1,10 +1,10 @@
package foo.bar;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.arch.persistence.room.EntityDeletionOrUpdateAdapter;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.SharedSQLiteStatement;
-import android.arch.persistence.room.util.StringUtil;
+import androidx.room.EntityDeletionOrUpdateAdapter;
+import androidx.room.RoomDatabase;
+import androidx.room.SharedSQLiteStatement;
+import androidx.room.util.StringUtil;
+import androidx.sqlite.db.SupportSQLiteStatement;
import java.lang.Override;
import java.lang.String;
import java.lang.StringBuilder;
@@ -12,7 +12,7 @@
import java.util.List;
import javax.annotation.Generated;
-@Generated("android.arch.persistence.room.RoomProcessor")
+@Generated("androidx.room.RoomProcessor")
@SuppressWarnings("unchecked")
public class DeletionDao_Impl implements DeletionDao {
private final RoomDatabase __db;
diff --git a/room/compiler/src/test/data/daoWriter/output/UpdateDao.java b/room/compiler/src/test/data/daoWriter/output/UpdateDao.java
index 8dc728f..7cab05a 100644
--- a/room/compiler/src/test/data/daoWriter/output/UpdateDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/UpdateDao.java
@@ -1,16 +1,16 @@
package foo.bar;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.arch.persistence.room.EntityDeletionOrUpdateAdapter;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.SharedSQLiteStatement;
+import androidx.room.EntityDeletionOrUpdateAdapter;
+import androidx.room.RoomDatabase;
+import androidx.room.SharedSQLiteStatement;
+import androidx.sqlite.db.SupportSQLiteStatement;
import java.lang.Override;
import java.lang.String;
import java.lang.SuppressWarnings;
import java.util.List;
import javax.annotation.Generated;
-@Generated("android.arch.persistence.room.RoomProcessor")
+@Generated("androidx.room.RoomProcessor")
@SuppressWarnings("unchecked")
public class UpdateDao_Impl implements UpdateDao {
private final RoomDatabase __db;
diff --git a/room/compiler/src/test/data/daoWriter/output/WriterDao.java b/room/compiler/src/test/data/daoWriter/output/WriterDao.java
index 04cb0bf..829dad4 100644
--- a/room/compiler/src/test/data/daoWriter/output/WriterDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/WriterDao.java
@@ -16,9 +16,9 @@
package foo.bar;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.arch.persistence.room.EntityInsertionAdapter;
-import android.arch.persistence.room.RoomDatabase;
+import androidx.room.EntityInsertionAdapter;
+import androidx.room.RoomDatabase;
+import androidx.sqlite.db.SupportSQLiteStatement;
import java.lang.Override;
import java.lang.String;
@@ -26,7 +26,7 @@
import java.util.List;
import javax.annotation.Generated;
-@Generated("android.arch.persistence.room.RoomProcessor")
+@Generated("androidx.room.RoomProcessor")
@SuppressWarnings("unchecked")
public class WriterDao_Impl implements WriterDao {
private final RoomDatabase __db;
diff --git a/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java
index 674e57e..537e7fe 100644
--- a/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/input/ComplexDatabase.java
@@ -15,7 +15,7 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+import androidx.room.*;
import java.lang.SuppressWarnings;
import java.util.List;
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index 17592a6..7f82dcd 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -1,17 +1,17 @@
package foo.bar;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.SupportSQLiteOpenHelper.Callback;
-import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration;
-import android.arch.persistence.room.DatabaseConfiguration;
-import android.arch.persistence.room.InvalidationTracker;
-import android.arch.persistence.room.RoomOpenHelper;
-import android.arch.persistence.room.RoomOpenHelper.Delegate;
-import android.arch.persistence.room.util.TableInfo;
-import android.arch.persistence.room.util.TableInfo.Column;
-import android.arch.persistence.room.util.TableInfo.ForeignKey;
-import android.arch.persistence.room.util.TableInfo.Index;
+import androidx.room.DatabaseConfiguration;
+import androidx.room.InvalidationTracker;
+import androidx.room.RoomOpenHelper;
+import androidx.room.RoomOpenHelper.Delegate;
+import androidx.room.util.TableInfo;
+import androidx.room.util.TableInfo.Column;
+import androidx.room.util.TableInfo.ForeignKey;
+import androidx.room.util.TableInfo.Index;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback;
+import androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration;
import java.lang.IllegalStateException;
import java.lang.Override;
import java.lang.String;
@@ -20,7 +20,7 @@
import java.util.HashSet;
import javax.annotation.Generated;
-@Generated("android.arch.persistence.room.RoomProcessor")
+@Generated("androidx.room.RoomProcessor")
@SuppressWarnings("unchecked")
public class ComplexDatabase_Impl extends ComplexDatabase {
private volatile ComplexDao _complexDao;
@@ -100,6 +100,8 @@
super.setTransactionSuccessful();
} finally {
super.endTransaction();
+ _db.query("PRAGMA wal_checkpoint(FULL)").close();
+ _db.execSQL("VACUUM");
}
}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/parser/SqlParserTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/parser/SqlParserTest.kt
deleted file mode 100644
index e6e8c3b..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/parser/SqlParserTest.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright (C) 2016 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.parser
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.not
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class SqlParserTest {
-
- @Test
- fun multipleQueries() {
- assertErrors("SELECT * FROM users; SELECT * FROM books;",
- ParserErrors.NOT_ONE_QUERY)
- }
-
- @Test
- fun empty() {
- assertErrors("", ParserErrors.NOT_ONE_QUERY)
- }
-
- @Test
- fun deleteQuery() {
- val parsed = SqlParser.parse("DELETE FROM users where id > 3")
- assertThat(parsed.errors, `is`(emptyList()))
- assertThat(parsed.type, `is`(QueryType.DELETE))
- }
-
- @Test
- fun badDeleteQuery() {
- assertErrors("delete from user where mAge >= :min && mAge <= :max",
- "no viable alternative at input 'delete from user where mAge >= :min &&'")
- }
-
- @Test
- fun updateQuery() {
- val parsed = SqlParser.parse("UPDATE users set name = :name where id = :id")
- assertThat(parsed.errors, `is`(emptyList()))
- assertThat(parsed.type, `is`(QueryType.UPDATE))
- }
-
- @Test
- fun explain() {
- assertErrors("EXPLAIN QUERY PLAN SELECT * FROM users",
- ParserErrors.invalidQueryType(QueryType.EXPLAIN))
- }
-
- @Test
- fun validColumnNames() {
- listOf("f", "fo", "f2", "f 2", "foo_2", "foo-2", "_", "foo bar baz",
- "foo 2 baz", "_baz", "fooBar", "2", "*", "foo*2", "dsa$", "\$fsa",
- "-bar", "ÅŸoöÄŸüı").forEach {
- assertThat("name: $it", SqlParser.isValidIdentifier(it), `is`(true))
- }
- }
-
- @Test
- fun invalidColumnNames() {
- listOf("", " ", "fd`a`", "f`a", "`a", "\"foo bar\"", "\"", "`").forEach {
- assertThat("name: $it", SqlParser.isValidIdentifier(it), `is`(false))
- }
- }
-
- @Test
- fun extractTableNames() {
- assertThat(SqlParser.parse("select * from users").tables,
- `is`(setOf(Table("users", "users"))))
- assertThat(SqlParser.parse("select * from users as ux").tables,
- `is`(setOf(Table("users", "ux"))))
- assertThat(SqlParser.parse("select * from (select * from books)").tables,
- `is`(setOf(Table("books", "books"))))
- assertThat(SqlParser.parse("select x.id from (select * from books) as x").tables,
- `is`(setOf(Table("books", "books"))))
- }
-
- @Test
- fun unescapeTableNames() {
- assertThat(SqlParser.parse("select * from `users`").tables,
- `is`(setOf(Table("users", "users"))))
- assertThat(SqlParser.parse("select * from \"users\"").tables,
- `is`(setOf(Table("users", "users"))))
- assertThat(SqlParser.parse("select * from 'users'").tables,
- `is`(setOf(Table("users", "users"))))
- }
-
- @Test
- fun tablePrefixInInsert_set() {
- // this is an invalid query, b/64539805
- val query = SqlParser.parse("UPDATE trips SET trips.title=:title")
- assertThat(query.errors, not(emptyList()))
- }
-
- @Test
- fun tablePrefixInInsert_where() {
- val query = SqlParser.parse("UPDATE trips SET title=:title WHERE trips.id=:id")
- assertThat(query.errors, `is`(emptyList()))
- }
-
- @Test
- fun tablePrefixInSelect_projection() {
- val query = SqlParser.parse("SELECT a.name, b.last_name from user a, book b")
- assertThat(query.errors, `is`(emptyList()))
- assertThat(query.tables, `is`(setOf(Table("user", "a"),
- Table("book", "b"))))
- }
-
- @Test
- fun tablePrefixInSelect_where() {
- val query = SqlParser.parse("SELECT a.name, b.last_name from user a, book b" +
- " WHERE a.name = b.name")
- assertThat(query.errors, `is`(emptyList()))
- assertThat(query.tables, `is`(setOf(Table("user", "a"),
- Table("book", "b"))))
- }
-
- @Test
- fun findBindVariables() {
- assertVariables("select * from users")
- assertVariables("select * from users where name like ?", "?")
- assertVariables("select * from users where name like :name", ":name")
- assertVariables("select * from users where name like ?2", "?2")
- assertVariables("select * from users where name like ?2 OR name LIKE ?1", "?2", "?1")
- assertVariables("select * from users where name like @a", "@a")
- assertVariables("select * from users where name like \$a", "\$a")
- }
-
- @Test
- fun indexedVariablesError() {
- assertErrors("select * from users where name like ?",
- ParserErrors.ANONYMOUS_BIND_ARGUMENT)
- assertErrors("select * from users where name like ? or last_name like ?",
- ParserErrors.ANONYMOUS_BIND_ARGUMENT)
- assertErrors("select * from users where name like ?1",
- ParserErrors.cannotUseVariableIndices("?1", 36))
- }
-
- @Test
- fun foo() {
- assertSections("select * from users where name like ?",
- Section.text("select * from users where name like "),
- Section.bindVar("?"))
-
- assertSections("select * from users where name like :name AND last_name like :lastName",
- Section.text("select * from users where name like "),
- Section.bindVar(":name"),
- Section.text(" AND last_name like "),
- Section.bindVar(":lastName"))
-
- assertSections("select * from users where name \nlike :name AND last_name like :lastName",
- Section.text("select * from users where name "),
- Section.newline(),
- Section.text("like "),
- Section.bindVar(":name"),
- Section.text(" AND last_name like "),
- Section.bindVar(":lastName"))
-
- assertSections("select * from users where name like :name \nAND last_name like :lastName",
- Section.text("select * from users where name like "),
- Section.bindVar(":name"),
- Section.text(" "),
- Section.newline(),
- Section.text("AND last_name like "),
- Section.bindVar(":lastName"))
-
- assertSections("select * from users where name like :name \nAND last_name like \n:lastName",
- Section.text("select * from users where name like "),
- Section.bindVar(":name"),
- Section.text(" "),
- Section.newline(),
- Section.text("AND last_name like "),
- Section.newline(),
- Section.bindVar(":lastName"))
- }
-
- fun assertVariables(query: String, vararg expected: String) {
- assertThat((SqlParser.parse(query)).inputs.map { it.text }, `is`(expected.toList()))
- }
-
- fun assertErrors(query: String, vararg errors: String) {
- assertThat((SqlParser.parse(query)).errors, `is`(errors.toList()))
- }
-
- fun assertSections(query: String, vararg sections: Section) {
- assertThat(SqlParser.parse(query).sections, `is`(sections.toList()))
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/BaseDaoTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/BaseDaoTest.kt
deleted file mode 100644
index 19430af..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/BaseDaoTest.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package android.arch.persistence.room.processor
-
-import COMMON
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.vo.Dao
-import android.arch.persistence.room.writer.DaoWriter
-import com.google.auto.common.MoreTypes
-import com.google.testing.compile.JavaFileObjects
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import simpleRun
-
-/**
- * we don't assert much in these tests since if type resolution fails, compilation fails.
- */
-@RunWith(JUnit4::class)
-class BaseDaoTest {
- private fun String.toJFO(qName: String) = JavaFileObjects.forSourceLines(qName, this)
-
- @Test
- fun insert() {
- baseDao("""
- @Insert
- void insertMe(T t);
- """) { dao ->
- assertThat(dao.insertionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun insertArray() {
- baseDao("""
- @Insert
- void insertMe(T[] t);
- """) { dao ->
- assertThat(dao.insertionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun insertVarArg() {
- baseDao("""
- @Insert
- void insertMe(T... t);
- """) { dao ->
- assertThat(dao.insertionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun insertList() {
- baseDao("""
- @Insert
- void insertMe(List<T> t);
- """) { dao ->
- assertThat(dao.insertionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun delete() {
- baseDao("""
- @Delete
- void deleteMe(T t);
- """) { dao ->
- assertThat(dao.deletionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun deleteArray() {
- baseDao("""
- @Delete
- void deleteMe(T[] t);
- """) { dao ->
- assertThat(dao.deletionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun deleteVarArg() {
- baseDao("""
- @Delete
- void deleteMe(T... t);
- """) { dao ->
- assertThat(dao.deletionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun deleteList() {
- baseDao("""
- @Delete
- void deleteMe(List<T> t);
- """) { dao ->
- assertThat(dao.deletionMethods.size, `is`(1))
- }
- }
-
- @Test
- fun update() {
- baseDao("""
- @Update
- void updateMe(T t);
- """) { dao ->
- assertThat(dao.updateMethods.size, `is`(1))
- }
- }
-
- @Test
- fun updateArray() {
- baseDao("""
- @Update
- void updateMe(T[] t);
- """) { dao ->
- assertThat(dao.updateMethods.size, `is`(1))
- }
- }
-
- @Test
- fun updateVarArg() {
- baseDao("""
- @Update
- void updateMe(T... t);
- """) { dao ->
- assertThat(dao.updateMethods.size, `is`(1))
- }
- }
-
- @Test
- fun updateList() {
- baseDao("""
- @Update
- void updateMe(List<T> t);
- """) { dao ->
- assertThat(dao.updateMethods.size, `is`(1))
- }
- }
-
- fun baseDao(code: String, handler: (Dao) -> Unit) {
- val baseClass = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import java.util.List;
-
- interface BaseDao<K, T> {
- $code
- }
- """.toJFO("foo.bar.BaseDao")
- val extension = """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Dao
- interface MyDao extends BaseDao<Integer, User> {
- }
- """.toJFO("foo.bar.MyDao")
- simpleRun(baseClass, extension, COMMON.USER) { invocation ->
- val daoElm = invocation.processingEnv.elementUtils.getTypeElement("foo.bar.MyDao")
- val dbType = MoreTypes.asDeclared(invocation.context.processingEnv.elementUtils
- .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType())
- val processedDao = DaoProcessor(invocation.context, daoElm, dbType, null).process()
- handler(processedDao)
- DaoWriter(processedDao, invocation.processingEnv).write(invocation.processingEnv)
- }.compilesWithoutError()
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/BaseEntityParserTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/BaseEntityParserTest.kt
deleted file mode 100644
index bd09a11..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/BaseEntityParserTest.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Embedded
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.Entity
-import android.support.annotation.NonNull
-import com.google.auto.common.MoreElements
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import javax.tools.JavaFileObject
-
-abstract class BaseEntityParserTest {
- companion object {
- const val ENTITY_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import android.support.annotation.NonNull;
- @Entity%s
- public class MyEntity %s {
- """
- const val ENTITY_SUFFIX = "}"
- }
-
- fun singleEntity(input: String, attributes: Map<String, String> = mapOf(),
- baseClass: String = "",
- jfos: List<JavaFileObject> = emptyList(),
- handler: (Entity, TestInvocation) -> Unit): CompileTester {
- val attributesReplacement: String
- if (attributes.isEmpty()) {
- attributesReplacement = ""
- } else {
- attributesReplacement = "(" +
- attributes.entries.map { "${it.key} = ${it.value}" }.joinToString(",") +
- ")".trimIndent()
- }
- val baseClassReplacement: String
- if (baseClass == "") {
- baseClassReplacement = ""
- } else {
- baseClassReplacement = " extends $baseClass"
- }
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfos + JavaFileObjects.forSourceString("foo.bar.MyEntity",
- ENTITY_PREFIX.format(attributesReplacement, baseClassReplacement)
- + input + ENTITY_SUFFIX
- ))
- .processedWith(TestProcessor.builder()
- .forAnnotations(android.arch.persistence.room.Entity::class,
- android.arch.persistence.room.PrimaryKey::class,
- android.arch.persistence.room.Ignore::class,
- Embedded::class,
- android.arch.persistence.room.ColumnInfo::class,
- NonNull::class)
- .nextRunHandler { invocation ->
- val entity = invocation.roundEnv
- .getElementsAnnotatedWith(
- android.arch.persistence.room.Entity::class.java)
- .first { it.toString() == "foo.bar.MyEntity" }
- val parser = EntityProcessor(invocation.context,
- MoreElements.asType(entity))
- val parsedQuery = parser.process()
- handler(parsedQuery, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/CustomConverterProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/CustomConverterProcessorTest.kt
deleted file mode 100644
index 067773a..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/CustomConverterProcessorTest.kt
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.TypeConverter
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
-import android.arch.persistence.room.processor.ProcessorErrors
- .TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_BE_PUBLIC
-import android.arch.persistence.room.processor.ProcessorErrors.TYPE_CONVERTER_UNBOUND_GENERIC
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.vo.CustomTypeConverter
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import com.squareup.javapoet.TypeVariableName
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import simpleRun
-import java.util.Date
-import javax.lang.model.element.Modifier
-import javax.tools.JavaFileObject
-
-@RunWith(JUnit4::class)
-class CustomConverterProcessorTest {
- companion object {
- val CONVERTER = ClassName.get("foo.bar", "MyConverter")!!
- val CONVERTER_QNAME = CONVERTER.packageName() + "." + CONVERTER.simpleName()
- val CONTAINER = JavaFileObjects.forSourceString("foo.bar.Container",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @TypeConverters(foo.bar.MyConverter.class)
- public class Container {}
- """)
- }
-
- @Test
- fun validCase() {
- singleClass(createConverter(TypeName.SHORT.box(), TypeName.CHAR.box())) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.SHORT.box()))
- assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
- }.compilesWithoutError()
- }
-
- @Test
- fun primitiveFrom() {
- singleClass(createConverter(TypeName.SHORT, TypeName.CHAR.box())) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
- assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
- }.compilesWithoutError()
- }
-
- @Test
- fun primitiveTo() {
- singleClass(createConverter(TypeName.INT.box(), TypeName.DOUBLE)) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.INT.box()))
- assertThat(converter?.toTypeName, `is`(TypeName.DOUBLE))
- }.compilesWithoutError()
- }
-
- @Test
- fun primitiveBoth() {
- singleClass(createConverter(TypeName.INT, TypeName.DOUBLE)) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.INT))
- assertThat(converter?.toTypeName, `is`(TypeName.DOUBLE))
- }.compilesWithoutError()
- }
-
- @Test
- fun nonNullButNotBoxed() {
- val string = String::class.typeName()
- val date = Date::class.typeName()
- singleClass(createConverter(string, date)) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(string as TypeName))
- assertThat(converter?.toTypeName, `is`(date as TypeName))
- }
- }
-
- @Test
- fun parametrizedTypeUnbound() {
- val typeVarT = TypeVariableName.get("T")
- val list = ParameterizedTypeName.get(List::class.typeName(), typeVarT)
- val typeVarK = TypeVariableName.get("K")
- val map = ParameterizedTypeName.get(Map::class.typeName(), typeVarK, typeVarT)
- singleClass(createConverter(list, map, listOf(typeVarK, typeVarT))) {
- _, _ ->
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_UNBOUND_GENERIC)
- }
-
- @Test
- fun parametrizedTypeSpecific() {
- val string = String::class.typeName()
- val date = Date::class.typeName()
- val list = ParameterizedTypeName.get(List::class.typeName(), string)
- val map = ParameterizedTypeName.get(Map::class.typeName(), string, date)
- singleClass(createConverter(list, map)) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(list as TypeName))
- assertThat(converter?.toTypeName, `is`(map as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun testNoConverters() {
- singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
- """
- package ${CONVERTER.packageName()};
- public class ${CONVERTER.simpleName()} {
- }
- """)) { _, _ ->
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_EMPTY_CLASS)
- }
-
- @Test
- fun checkNoArgConstructor() {
- singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
- """
- package ${CONVERTER.packageName()};
- import android.arch.persistence.room.TypeConverter;
-
- public class ${CONVERTER.simpleName()} {
- public ${CONVERTER.simpleName()}(int x) {}
- @TypeConverter
- public int x(short y) {return 0;}
- }
- """)) { _, _ ->
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
- }
-
- @Test
- fun checkNoArgConstructor_withStatic() {
- singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
- """
- package ${CONVERTER.packageName()};
- import android.arch.persistence.room.TypeConverter;
-
- public class ${CONVERTER.simpleName()} {
- public ${CONVERTER.simpleName()}(int x) {}
- @TypeConverter
- public static int x(short y) {return 0;}
- }
- """)) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
- assertThat(converter?.toTypeName, `is`(TypeName.INT))
- assertThat(converter?.isStatic, `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun checkPublic() {
- singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
- """
- package ${CONVERTER.packageName()};
- import android.arch.persistence.room.TypeConverter;
-
- public class ${CONVERTER.simpleName()} {
- @TypeConverter static int x(short y) {return 0;}
- @TypeConverter private static int y(boolean y) {return 0;}
- }
- """)) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
- assertThat(converter?.toTypeName, `is`(TypeName.INT))
- assertThat(converter?.isStatic, `is`(true))
- }.failsToCompile().withErrorContaining(TYPE_CONVERTER_MUST_BE_PUBLIC).and()
- .withErrorCount(2)
- }
-
- @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
- @Test
- fun parametrizedTypeBoundViaParent() {
- val typeVarT = TypeVariableName.get("T")
- val list = ParameterizedTypeName.get(List::class.typeName(), typeVarT)
- val typeVarK = TypeVariableName.get("K")
- val map = ParameterizedTypeName.get(Map::class.typeName(), typeVarK, typeVarT)
-
- val baseConverter = createConverter(list, map, listOf(typeVarT, typeVarK))
- val extendingQName = "foo.bar.Extending"
- val extendingClass = JavaFileObjects.forSourceString(extendingQName,
- "package foo.bar;\n" +
- TypeSpec.classBuilder(ClassName.bestGuess(extendingQName)).apply {
- superclass(
- ParameterizedTypeName.get(CONVERTER, String::class.typeName(),
- Integer::class.typeName()))
- }.build().toString())
-
- simpleRun(baseConverter, extendingClass) { invocation ->
- val element = invocation.processingEnv.elementUtils.getTypeElement(extendingQName)
- val converter = CustomConverterProcessor(invocation.context, element)
- .process().firstOrNull()
- assertThat(converter?.fromTypeName, `is`(ParameterizedTypeName.get(
- List::class.typeName(), String::class.typeName()) as TypeName
- ))
- assertThat(converter?.toTypeName, `is`(ParameterizedTypeName.get(Map::class.typeName(),
- Integer::class.typeName(), String::class.typeName()) as TypeName
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun checkDuplicates() {
- singleClass(
- createConverter(TypeName.SHORT.box(), TypeName.CHAR.box(), duplicate = true)
- ) { converter, _ ->
- assertThat(converter?.fromTypeName, `is`(TypeName.SHORT.box()))
- assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
- }.failsToCompile().withErrorContaining("Multiple methods define the same conversion")
- }
-
- private fun createConverter(
- from: TypeName,
- to: TypeName,
- typeVariables: List<TypeVariableName> = emptyList(),
- duplicate: Boolean = false
- ): JavaFileObject {
- val code = TypeSpec.classBuilder(CONVERTER).apply {
- addTypeVariables(typeVariables)
- addModifiers(Modifier.PUBLIC)
- fun buildMethod(name: String) = MethodSpec.methodBuilder(name).apply {
- addAnnotation(TypeConverter::class.java)
- addModifiers(Modifier.PUBLIC)
- returns(to)
- addParameter(ParameterSpec.builder(from, "input").build())
- if (to.isPrimitive) {
- addStatement("return 0")
- } else {
- addStatement("return null")
- }
- }.build()
- addMethod(buildMethod("convertF"))
- if (duplicate) {
- addMethod(buildMethod("convertF2"))
- }
- }.build().toString()
- return JavaFileObjects.forSourceString(CONVERTER.toString(),
- "package ${CONVERTER.packageName()};\n$code")
- }
-
- private fun singleClass(
- vararg jfo: JavaFileObject,
- handler: (CustomTypeConverter?, TestInvocation) -> Unit
- ): CompileTester {
- return simpleRun(*((jfo.toList() + CONTAINER).toTypedArray())) { invocation ->
- val processed = CustomConverterProcessor.findConverters(invocation.context,
- invocation.processingEnv.elementUtils.getTypeElement("foo.bar.Container"))
- handler(processed.converters.firstOrNull()?.custom, invocation)
- }
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
deleted file mode 100644
index aa717f8..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import COMMON
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.Dao
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import createVerifierFromEntities
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-class DaoProcessorTest(val enableVerification: Boolean) {
- companion object {
- const val DAO_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- """
- @Parameterized.Parameters(name = "enableDbVerification={0}")
- @JvmStatic
- fun getParams() = arrayOf(true, false)
- }
-
- @Test
- fun testNonAbstract() {
- singleDao("@Dao public class MyDao {}") { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE)
- }
-
- @Test
- fun testAbstractMethodWithoutQuery() {
- singleDao("""
- @Dao public interface MyDao {
- int getFoo();
- }
- """) { _, _ ->
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.ABSTRACT_METHOD_IN_DAO_MISSING_ANY_ANNOTATION)
- }
-
- @Test
- fun testBothAnnotations() {
- singleDao("""
- @Dao public interface MyDao {
- @Query("select 1")
- @Insert
- int getFoo(int x);
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
- }
-
- @Test
- fun testAbstractClass() {
- singleDao("""
- @Dao abstract class MyDao {
- @Query("SELECT uid FROM User")
- abstract int[] getIds();
- }
- """) { dao, _ ->
- assertThat(dao.queryMethods.size, `is`(1))
- val method = dao.queryMethods.first()
- assertThat(method.name, `is`("getIds"))
- }.compilesWithoutError()
- }
-
- @Test
- fun testInterface() {
- singleDao("""
- @Dao interface MyDao {
- @Query("SELECT uid FROM User")
- abstract int[] getIds();
- }
- """) { dao, _ ->
- assertThat(dao.queryMethods.size, `is`(1))
- val method = dao.queryMethods.first()
- assertThat(method.name, `is`("getIds"))
- }.compilesWithoutError()
- }
-
- @Test
- fun testWithInsertAndQuery() {
- singleDao("""
- @Dao abstract class MyDao {
- @Query("SELECT uid FROM User")
- abstract int[] getIds();
- @Insert
- abstract void insert(User user);
- }
- """) { dao, _ ->
- assertThat(dao.queryMethods.size, `is`(1))
- val method = dao.queryMethods.first()
- assertThat(method.name, `is`("getIds"))
- assertThat(dao.insertionMethods.size, `is`(1))
- val insertMethod = dao.insertionMethods.first()
- assertThat(insertMethod.name, `is`("insert"))
- }.compilesWithoutError()
- }
-
- @Test
- fun skipQueryVerification() {
- singleDao("""
- @Dao @SkipQueryVerification interface MyDao {
- @Query("SELECT nonExistingField FROM User")
- abstract int[] getIds();
- }
- """) { dao, _ ->
- assertThat(dao.queryMethods.size, `is`(1))
- val method = dao.queryMethods.first()
- assertThat(method.name, `is`("getIds"))
- }.compilesWithoutError()
- }
-
- @Test
- fun suppressedWarnings() {
- singleDao("""
- @SuppressWarnings({"ALL", RoomWarnings.CURSOR_MISMATCH})
- @Dao interface MyDao {
- @Query("SELECT * from user")
- abstract User users();
- }
- """) { dao, invocation ->
- val dbType = MoreTypes.asDeclared(invocation.context.processingEnv.elementUtils
- .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType())
- val daoProcessor = DaoProcessor(invocation.context, dao.element, dbType, null)
- assertThat(daoProcessor.context.logger
- .suppressedWarnings, `is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH)))
-
- dao.queryMethods.forEach {
- assertThat(QueryMethodProcessor(
- baseContext = daoProcessor.context,
- containing = MoreTypes.asDeclared(dao.element.asType()),
- executableElement = it.element,
- dbVerifier = null).context.logger.suppressedWarnings,
- `is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH)))
- }
- }.compilesWithoutError()
- }
-
- @Test
- fun suppressedWarningsInheritance() {
- singleDao("""
- @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
- @Dao interface MyDao {
- @SuppressWarnings("ALL")
- @Query("SELECT * from user")
- abstract User users();
- }
- """) { dao, invocation ->
- val dbType = MoreTypes.asDeclared(invocation.context.processingEnv.elementUtils
- .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType())
- val daoProcessor = DaoProcessor(invocation.context, dao.element, dbType, null)
- assertThat(daoProcessor.context.logger
- .suppressedWarnings, `is`(setOf(Warning.CURSOR_MISMATCH)))
-
- dao.queryMethods.forEach {
- assertThat(QueryMethodProcessor(
- baseContext = daoProcessor.context,
- containing = MoreTypes.asDeclared(dao.element.asType()),
- executableElement = it.element,
- dbVerifier = null).context.logger.suppressedWarnings,
- `is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH)))
- }
- }.compilesWithoutError()
- }
-
- @Test
- fun query_warnIfTransactionIsMissingForRelation() {
- if (!enableVerification) {
- return
- }
- singleDao(
- """
- @Dao interface MyDao {
- static class Merged extends User {
- @Relation(parentColumn = "name", entityColumn = "lastName",
- entity = User.class)
- java.util.List<User> users;
- }
- @Query("select * from user")
- abstract java.util.List<Merged> loadUsers();
- }
- """
- ) { dao, _ ->
- assertThat(dao.queryMethods.size, `is`(1))
- assertThat(dao.queryMethods.first().inTransaction, `is`(false))
- }.compilesWithoutError()
- .withWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
- }
-
- @Test
- fun query_dontWarnIfTransactionIsMissingForRelation_suppressed() {
- if (!enableVerification) {
- return
- }
- singleDao(
- """
- @Dao interface MyDao {
- static class Merged extends User {
- @Relation(parentColumn = "name", entityColumn = "lastName",
- entity = User.class)
- java.util.List<User> users;
- }
- @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
- @Query("select * from user")
- abstract java.util.List<Merged> loadUsers();
- }
- """
- ) { dao, _ ->
- assertThat(dao.queryMethods.size, `is`(1))
- assertThat(dao.queryMethods.first().inTransaction, `is`(false))
- }.compilesWithoutError()
- .withWarningCount(0)
- }
-
- @Test
- fun query_dontWarnIfTransactionNotIsMissingForRelation() {
- if (!enableVerification) {
- return
- }
- singleDao(
- """
- @Dao interface MyDao {
- static class Merged extends User {
- @Relation(parentColumn = "name", entityColumn = "lastName",
- entity = User.class)
- java.util.List<User> users;
- }
- @Transaction
- @Query("select * from user")
- abstract java.util.List<Merged> loadUsers();
- }
- """
- ) { dao, _ ->
- // test sanity
- assertThat(dao.queryMethods.size, `is`(1))
- assertThat(dao.queryMethods.first().inTransaction, `is`(true))
- }.compilesWithoutError()
- .withWarningCount(0)
- }
-
- fun singleDao(vararg inputs: String, handler: (Dao, TestInvocation) -> Unit):
- CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyDao",
- DAO_PREFIX + inputs.joinToString("\n")
- ), COMMON.USER))
- .processedWith(TestProcessor.builder()
- .forAnnotations(android.arch.persistence.room.Dao::class,
- android.arch.persistence.room.Entity::class,
- android.arch.persistence.room.Relation::class,
- android.arch.persistence.room.Transaction::class,
- android.arch.persistence.room.ColumnInfo::class,
- android.arch.persistence.room.PrimaryKey::class,
- android.arch.persistence.room.Query::class)
- .nextRunHandler { invocation ->
- val dao = invocation.roundEnv
- .getElementsAnnotatedWith(
- android.arch.persistence.room.Dao::class.java)
- .first()
- val dbVerifier = if (enableVerification) {
- createVerifierFromEntities(invocation)
- } else {
- null
- }
- val dbType = MoreTypes.asDeclared(
- invocation.context.processingEnv.elementUtils
- .getTypeElement(RoomTypeNames.ROOM_DB.toString())
- .asType())
- val parser = DaoProcessor(invocation.context,
- MoreElements.asType(dao), dbType, dbVerifier)
-
- val parsedDao = parser.process()
- handler(parsedDao, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DatabaseProcessorTest.kt
deleted file mode 100644
index d192c8f..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DatabaseProcessorTest.kt
+++ /dev/null
@@ -1,711 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.RoomProcessor
-import android.arch.persistence.room.solver.query.result.EntityRowAdapter
-import android.arch.persistence.room.solver.query.result.PojoRowAdapter
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.Database
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.MoreElements
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.ClassName
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.instanceOf
-import org.hamcrest.CoreMatchers.not
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.CoreMatchers.sameInstance
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.tools.JavaFileObject
-import javax.tools.StandardLocation
-
-@RunWith(JUnit4::class)
-class DatabaseProcessorTest {
- companion object {
- const val DATABASE_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- """
- val DB1: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Db1",
- """
- $DATABASE_PREFIX
- @Database(entities = {Book.class}, version = 42)
- public abstract class Db1 extends RoomDatabase {
- abstract BookDao bookDao();
- }
- """)
- val DB2: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Db2",
- """
- $DATABASE_PREFIX
- @Database(entities = {Book.class}, version = 42)
- public abstract class Db2 extends RoomDatabase {
- abstract BookDao bookDao();
- }
- """)
- val DB3: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Db3",
- """
- $DATABASE_PREFIX
- @Database(entities = {Book.class}, version = 42)
- public abstract class Db3 extends RoomDatabase {
- }
- """)
- val USER: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.User",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- public class User {
- @PrimaryKey
- int uid;
- }
- """)
- val USER_DAO: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.UserDao",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Dao
- public interface UserDao {
- @Query("SELECT * FROM user")
- public java.util.List<User> loadAll();
-
- @Insert
- public void insert(User... users);
-
- @Query("SELECT * FROM user where uid = :uid")
- public User loadOne(int uid);
-
- @TypeConverters(Converter.class)
- @Query("SELECT * FROM user where uid = :uid")
- public User loadWithConverter(int uid);
-
- @Query("SELECT * FROM user where uid = :uid")
- public Pojo loadOnePojo(int uid);
-
- @Query("SELECT * FROM user")
- public java.util.List<Pojo> loadAllPojos();
-
- @TypeConverters(Converter.class)
- @Query("SELECT * FROM user where uid = :uid")
- public Pojo loadPojoWithConverter(int uid);
-
- public static class Converter {
- @TypeConverter
- public static java.util.Date foo(Long input) {return null;}
- }
-
- public static class Pojo {
- public int uid;
- }
- }
- """)
- val BOOK: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Book",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- public class Book {
- @PrimaryKey
- int bookId;
- }
- """)
- val BOOK_DAO: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.BookDao",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Dao
- public interface BookDao {
- @Query("SELECT * FROM book")
- public java.util.List<Book> loadAllBooks();
- @Insert
- public void insert(Book book);
- }
- """)
- }
-
- @Test
- fun simple() {
- singleDb("""
- @Database(entities = {User.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract UserDao userDao();
- }
- """, USER, USER_DAO) { db, _ ->
- assertThat(db.daoMethods.size, `is`(1))
- assertThat(db.entities.size, `is`(1))
- }.compilesWithoutError()
- }
-
- @Test
- fun multiple() {
- singleDb("""
- @Database(entities = {User.class, Book.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract UserDao userDao();
- abstract BookDao bookDao();
- }
- """, USER, USER_DAO, BOOK, BOOK_DAO) { db, _ ->
- assertThat(db.daoMethods.size, `is`(2))
- assertThat(db.entities.size, `is`(2))
- assertThat(db.daoMethods.map { it.name }, `is`(listOf("userDao", "bookDao")))
- assertThat(db.entities.map { it.type.toString() },
- `is`(listOf("foo.bar.User", "foo.bar.Book")))
- }.compilesWithoutError()
- }
-
- @Test
- fun detectMissingBaseClass() {
- singleDb("""
- @Database(entities = {User.class, Book.class}, version = 42)
- public abstract class MyDb {
- }
- """, USER, BOOK) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
- }
-
- @Test
- fun detectMissingTable() {
- singleDb(
- """
- @Database(entities = {Book.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract BookDao bookDao();
- }
- """, BOOK, JavaFileObjects.forSourceString("foo.bar.BookDao",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Dao
- public interface BookDao {
- @Query("SELECT * FROM nonExistentTable")
- public java.util.List<Book> loadAllBooks();
- }
- """)) { _, _ ->
- }.failsToCompile().withErrorContaining("no such table: nonExistentTable")
- }
-
- @Test
- fun detectDuplicateTableNames() {
- singleDb("""
- @Database(entities = {User.class, AnotherClass.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract UserDao userDao();
- }
- """, USER, USER_DAO, JavaFileObjects.forSourceString("foo.bar.AnotherClass",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(tableName="user")
- public class AnotherClass {
- @PrimaryKey
- int uid;
- }
- """)) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.duplicateTableNames("user",
- listOf("foo.bar.User", "foo.bar.AnotherClass"))
- )
- }
-
- @Test
- fun skipBadQueryVerification() {
- singleDb(
- """
- @SkipQueryVerification
- @Database(entities = {Book.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract BookDao bookDao();
- }
- """, BOOK, JavaFileObjects.forSourceString("foo.bar.BookDao",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Dao
- public interface BookDao {
- @Query("SELECT nonExistingField FROM Book")
- public java.util.List<Book> loadAllBooks();
- }
- """)) { _, _ ->
- }.compilesWithoutError()
- }
-
- @Test
- fun multipleDatabases() {
- val db1_2 = JavaFileObjects.forSourceString("foo.barx.Db1",
- """
- package foo.barx;
- import android.arch.persistence.room.*;
- import foo.bar.*;
- @Database(entities = {Book.class}, version = 42)
- public abstract class Db1 extends RoomDatabase {
- abstract BookDao bookDao();
- }
- """)
- Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(BOOK, BOOK_DAO, DB1, DB2, db1_2))
- .processedWith(RoomProcessor())
- .compilesWithoutError()
- .and()
- .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar", "Db1_Impl.class")
- .and()
- .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar", "Db2_Impl.class")
- .and()
- .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.barx", "Db1_Impl.class")
- .and()
- .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar",
- "BookDao_Db1_0_Impl.class")
- .and()
- .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar",
- "BookDao_Db1_1_Impl.class")
- .and()
- .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar",
- "BookDao_Db2_Impl.class")
- }
-
- @Test
- fun twoDaoMethodsForTheSameDao() {
- singleDb(
- """
- @Database(entities = {User.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract UserDao userDao();
- abstract UserDao userDao2();
- }
- """, USER, USER_DAO) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.DAO_METHOD_CONFLICTS_WITH_OTHERS)
- .and()
- .withErrorContaining(ProcessorErrors.duplicateDao(
- ClassName.get("foo.bar", "UserDao"), listOf("userDao", "userDao2")
- ))
- }
-
- @Test
- fun suppressedWarnings() {
- singleDb(
- """
- @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
- @Database(entities = {User.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract UserDao userDao();
- }
- """, USER, USER_DAO) { db, invocation ->
- assertThat(DatabaseProcessor(invocation.context, db.element)
- .context.logger.suppressedWarnings, `is`(setOf(Warning.CURSOR_MISMATCH)))
- }.compilesWithoutError()
- }
-
- @Test
- fun duplicateIndexNames() {
- val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = {@Index(name ="index_name", value = {"name"})})
- public class Entity1 {
- @PrimaryKey
- int uid;
- String name;
- }
- """)
-
- val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = {@Index(name ="index_name", value = {"anotherName"})})
- public class Entity2 {
- @PrimaryKey
- int uid;
- String anotherName;
- }
- """)
- singleDb("""
- @Database(entities = {Entity1.class, Entity2.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- }
- """, entity1, entity2) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.duplicateIndexInDatabase("index_name",
- listOf("foo.bar.Entity1 > index_name", "foo.bar.Entity2 > index_name"))
- )
- }
-
- @Test
- fun foreignKey_missingParent() {
- val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(foreignKeys = @ForeignKey(entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name"))
- public class Entity1 {
- @PrimaryKey
- int uid;
- String name;
- }
- """)
- singleDb("""
- @Database(entities = {Entity1.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- }
- """, entity1, COMMON.USER) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.foreignKeyMissingParentEntityInDatabase("User", "foo.bar.Entity1")
- )
- }
-
- @Test
- fun foreignKey_missingParentIndex() {
- val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(foreignKeys = @ForeignKey(entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name"))
- public class Entity1 {
- @PrimaryKey
- int uid;
- String name;
- }
- """)
- singleDb("""
- @Database(entities = {Entity1.class, User.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- }
- """, entity1, COMMON.USER) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.foreignKeyMissingIndexInParent(
- parentEntity = COMMON.USER_TYPE_NAME.toString(),
- parentColumns = listOf("lastName"),
- childEntity = "foo.bar.Entity1",
- childColumns = listOf("name")
- )
- )
- }
-
- @Test
- fun foreignKey_goodWithPrimaryKey() {
- val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
- parentColumns = "uid",
- childColumns = "parentId"))
- public class Entity1 {
- @PrimaryKey
- int uid;
- int parentId;
- String name;
- }
- """)
-
- val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- public class Entity2 {
- @PrimaryKey
- int uid;
- String anotherName;
- }
- """)
- singleDb("""
- @Database(entities = {Entity1.class, Entity2.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- }
- """, entity1, entity2) { _, _ ->
- }.compilesWithoutError()
- }
-
- @Test
- fun foreignKey_missingParentColumn() {
- val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
- parentColumns = {"anotherName", "anotherName2"},
- childColumns = {"name", "name2"}))
- public class Entity1 {
- @PrimaryKey
- int uid;
- String name;
- String name2;
- }
- """)
-
- val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- public class Entity2 {
- @PrimaryKey
- int uid;
- String anotherName;
- }
- """)
- singleDb("""
- @Database(entities = {Entity1.class, Entity2.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- }
- """, entity1, entity2) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.foreignKeyParentColumnDoesNotExist("foo.bar.Entity2",
- "anotherName2", listOf("uid", "anotherName"))
- )
- }
-
- @Test
- fun foreignKey_goodWithIndex() {
- val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
- parentColumns = {"anotherName", "anotherName2"},
- childColumns = {"name", "name2"}))
- public class Entity1 {
- @PrimaryKey
- int uid;
- String name;
- String name2;
- }
- """)
-
- val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = @Index(value = {"anotherName2", "anotherName"}, unique = true))
- public class Entity2 {
- @PrimaryKey
- int uid;
- String anotherName;
- String anotherName2;
- }
- """)
- singleDb("""
- @Database(entities = {Entity1.class, Entity2.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- }
- """, entity1, entity2) { _, _ ->
- }.compilesWithoutError()
- }
-
- @Test
- fun insertNotAReferencedEntity() {
- singleDb("""
- @Database(entities = {User.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- abstract BookDao bookDao();
- }
- """, USER, USER_DAO, BOOK, BOOK_DAO) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.shortcutEntityIsNotInDatabase(
- database = "foo.bar.MyDb",
- dao = "foo.bar.BookDao",
- entity = "foo.bar.Book"
- )
- )
- }
-
- @Test
- fun cache_entity() {
- singleDb("""
- @Database(entities = {User.class}, version = 42)
- @SkipQueryVerification
- public abstract class MyDb extends RoomDatabase {
- public abstract MyUserDao userDao();
- @Dao
- interface MyUserDao {
- @Insert
- public void insert(User... users);
-
- @Query("SELECT * FROM user where uid = :uid")
- public User loadOne(int uid);
-
- @TypeConverters(Converter.class)
- @Query("SELECT * FROM user where uid = :uid")
- public User loadWithConverter(int uid);
- }
- public static class Converter {
- @TypeConverter
- public static java.util.Date foo(Long input) {return null;}
- }
- }
- """, USER, USER_DAO) { db, _ ->
- val userDao = db.daoMethods.first().dao
- val insertionMethod = userDao.insertionMethods.find { it.name == "insert" }
- assertThat(insertionMethod, notNullValue())
- val loadOne = userDao.queryMethods.find { it.name == "loadOne" }
- assertThat(loadOne, notNullValue())
- val adapter = loadOne?.queryResultBinder?.adapter?.rowAdapter
- assertThat("test sanity", adapter, instanceOf(EntityRowAdapter::class.java))
- val adapterEntity = (adapter as EntityRowAdapter).entity
- assertThat(insertionMethod?.entities?.values?.first(), sameInstance(adapterEntity))
-
- val withConverter = userDao.queryMethods.find { it.name == "loadWithConverter" }
- assertThat(withConverter, notNullValue())
- val convAdapter = withConverter?.queryResultBinder?.adapter?.rowAdapter
- assertThat("test sanity", adapter, instanceOf(EntityRowAdapter::class.java))
- val convAdapterEntity = (convAdapter as EntityRowAdapter).entity
- assertThat(insertionMethod?.entities?.values?.first(),
- not(sameInstance(convAdapterEntity)))
-
- assertThat(convAdapterEntity, notNullValue())
- assertThat(adapterEntity, notNullValue())
- }.compilesWithoutError()
- }
-
- @Test
- fun cache_pojo() {
- singleDb("""
- @Database(entities = {User.class}, version = 42)
- public abstract class MyDb extends RoomDatabase {
- public abstract UserDao userDao();
- }
- """, USER, USER_DAO) { db, _ ->
- val userDao = db.daoMethods.first().dao
- val loadOne = userDao.queryMethods.find { it.name == "loadOnePojo" }
- assertThat(loadOne, notNullValue())
- val adapter = loadOne?.queryResultBinder?.adapter?.rowAdapter
- assertThat("test sanity", adapter, instanceOf(PojoRowAdapter::class.java))
- val adapterPojo = (adapter as PojoRowAdapter).pojo
-
- val loadAll = userDao.queryMethods.find { it.name == "loadAllPojos" }
- assertThat(loadAll, notNullValue())
- val loadAllAdapter = loadAll?.queryResultBinder?.adapter?.rowAdapter
- assertThat("test sanity", loadAllAdapter, instanceOf(PojoRowAdapter::class.java))
- val loadAllPojo = (loadAllAdapter as PojoRowAdapter).pojo
- assertThat(adapter, not(sameInstance(loadAllAdapter)))
- assertThat(adapterPojo, sameInstance(loadAllPojo))
-
- val withConverter = userDao.queryMethods.find { it.name == "loadPojoWithConverter" }
- assertThat(withConverter, notNullValue())
- val convAdapter = withConverter?.queryResultBinder?.adapter?.rowAdapter
- assertThat("test sanity", adapter, instanceOf(PojoRowAdapter::class.java))
- val convAdapterPojo = (convAdapter as PojoRowAdapter).pojo
- assertThat(convAdapterPojo, notNullValue())
- assertThat(convAdapterPojo, not(sameInstance(adapterPojo)))
- }.compilesWithoutError()
- }
-
- @Test
- fun daoConstructor_RoomDatabase() {
- assertConstructor(listOf(DB1), "BookDao(RoomDatabase db) {}")
- .compilesWithoutError()
- }
-
- @Test
- fun daoConstructor_specificDatabase() {
- assertConstructor(listOf(DB1), "BookDao(Db1 db) {}")
- .compilesWithoutError()
- }
-
- @Test
- fun daoConstructor_wrongDatabase() {
- assertConstructor(listOf(DB1, DB3), "BookDao(Db3 db) {}")
- .failsToCompile()
- .withErrorContaining(ProcessorErrors
- .daoMustHaveMatchingConstructor("foo.bar.BookDao", "foo.bar.Db1"))
- }
-
- @Test
- fun daoConstructor_multipleDatabases_RoomDatabase() {
- assertConstructor(listOf(DB1, DB2), "BookDao(RoomDatabase db) {}")
- .compilesWithoutError()
- }
-
- @Test
- fun daoConstructor_multipleDatabases_specificDatabases() {
- assertConstructor(listOf(DB1, DB2), """
- BookDao(Db1 db) {}
- BookDao(Db2 db) {}
- """)
- .compilesWithoutError()
- }
-
- @Test
- fun daoConstructor_multipleDatabases_empty() {
- assertConstructor(listOf(DB1, DB2), """
- BookDao(Db1 db) {}
- BookDao() {} // Db2 uses this
- """)
- .compilesWithoutError()
- }
-
- @Test
- fun daoConstructor_multipleDatabases_noMatch() {
- assertConstructor(listOf(DB1, DB2), """
- BookDao(Db1 db) {}
- """)
- .failsToCompile()
- .withErrorContaining(ProcessorErrors
- .daoMustHaveMatchingConstructor("foo.bar.BookDao", "foo.bar.Db2"))
- }
-
- fun assertConstructor(dbs: List<JavaFileObject>, constructor: String): CompileTester {
- val bookDao = JavaFileObjects.forSourceString("foo.bar.BookDao",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Dao
- public abstract class BookDao {
- $constructor
- }
- """)
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(BOOK, bookDao) + dbs)
- .processedWith(RoomProcessor())
- }
-
- fun singleDb(input: String, vararg otherFiles: JavaFileObject,
- handler: (Database, TestInvocation) -> Unit): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(otherFiles.toMutableList()
- + JavaFileObjects.forSourceString("foo.bar.MyDb",
- DATABASE_PREFIX + input
- ))
- .processedWith(TestProcessor.builder()
- .forAnnotations(android.arch.persistence.room.Database::class)
- .nextRunHandler { invocation ->
- val entity = invocation.roundEnv
- .getElementsAnnotatedWith(
- android.arch.persistence.room.Database::class.java)
- .first()
- val parser = DatabaseProcessor(invocation.context,
- MoreElements.asType(entity))
- val parsedDb = parser.process()
- handler(parsedDb, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DeletionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DeletionMethodProcessorTest.kt
deleted file mode 100644
index 40546e8..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DeletionMethodProcessorTest.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Delete
-import android.arch.persistence.room.processor.ProcessorErrors.DELETION_MISSING_PARAMS
-import android.arch.persistence.room.processor.ProcessorErrors
- .DELETION_METHODS_MUST_RETURN_VOID_OR_INT
-import android.arch.persistence.room.vo.DeletionMethod
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-@RunWith(JUnit4::class)
-class DeletionMethodProcessorTest : ShortcutMethodProcessorTest<DeletionMethod>(Delete::class) {
- override fun invalidReturnTypeError(): String = DELETION_METHODS_MUST_RETURN_VOID_OR_INT
-
- override fun noParamsError(): String = DELETION_MISSING_PARAMS
-
- override fun process(baseContext: Context, containing: DeclaredType,
- executableElement: ExecutableElement): DeletionMethod {
- return DeletionMethodProcessor(baseContext, containing, executableElement).process()
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/EntityNameMatchingVariationsTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/EntityNameMatchingVariationsTest.kt
deleted file mode 100644
index 00dde74..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/EntityNameMatchingVariationsTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.vo.CallType
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.FieldGetter
-import android.arch.persistence.room.vo.FieldSetter
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import javax.lang.model.type.TypeKind.INT
-
-@RunWith(Parameterized::class)
-class EntityNameMatchingVariationsTest(triple: Triple<String, String, String>) :
- BaseEntityParserTest() {
- val fieldName = triple.first
- val getterName = triple.second
- val setterName = triple.third
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "{0}")
- fun params(): List<Triple<String, String, String>> {
- val result = arrayListOf<Triple<String, String, String>>()
- arrayListOf("x", "_x", "mX").forEach { field ->
- arrayListOf("getX", "x").forEach { getter ->
- arrayListOf("setX", "x").forEach { setter ->
- result.add(Triple(field, getter, setter))
- }
- }
- }
- return result
- }
- }
-
- @Test
- fun testSuccessfulParamToMethodMatching() {
- singleEntity("""
- @PrimaryKey
- private int $fieldName;
- public int $getterName() { return $fieldName; }
- public void $setterName(int id) { this.$fieldName = id; }
- """) { entity, invocation ->
- assertThat(entity.type.toString(), `is`("foo.bar.MyEntity"))
- assertThat(entity.fields.size, `is`(1))
- val field = entity.fields.first()
- val intType = invocation.processingEnv.typeUtils.getPrimitiveType(INT)
- assertThat(field, `is`(Field(
- element = field.element,
- name = fieldName,
- type = intType,
- columnName = fieldName,
- affinity = SQLTypeAffinity.INTEGER)))
- assertThat(field.setter, `is`(FieldSetter(setterName, intType, CallType.METHOD)))
- assertThat(field.getter, `is`(FieldGetter(getterName, intType, CallType.METHOD)))
- }.compilesWithoutError()
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/EntityProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/EntityProcessorTest.kt
deleted file mode 100644
index 95c70ca..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/EntityProcessorTest.kt
+++ /dev/null
@@ -1,1907 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import COMMON
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.processor.ProcessorErrors.RELATION_IN_ENTITY
-import android.arch.persistence.room.vo.CallType
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.FieldGetter
-import android.arch.persistence.room.vo.FieldSetter
-import android.arch.persistence.room.vo.Index
-import android.arch.persistence.room.vo.Pojo
-import com.google.testing.compile.JavaFileObjects
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.lang.model.type.TypeKind.INT
-
-@RunWith(JUnit4::class)
-class EntityProcessorTest : BaseEntityParserTest() {
- @Test
- fun simple() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public int getId() { return id; }
- public void setId(int id) { this.id = id; }
- """) { entity, invocation ->
- assertThat(entity.type.toString(), `is`("foo.bar.MyEntity"))
- assertThat(entity.fields.size, `is`(1))
- val field = entity.fields.first()
- val intType = invocation.processingEnv.typeUtils.getPrimitiveType(INT)
- assertThat(field, `is`(Field(
- element = field.element,
- name = "id",
- type = intType,
- columnName = "id",
- affinity = SQLTypeAffinity.INTEGER)))
- assertThat(field.setter, `is`(FieldSetter("setId", intType, CallType.METHOD)))
- assertThat(field.getter, `is`(FieldGetter("getId", intType, CallType.METHOD)))
- assertThat(entity.primaryKey.fields, `is`(listOf(field)))
- }.compilesWithoutError()
- }
-
- @Test
- fun noGetter() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {this.id = id;}
- """) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
- }
-
- @Test
- fun getterWithBadType() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public float getId() {return 0f;}
- public void setId(int id) {this.id = id;}
- """) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
- }
-
- @Test
- fun setterWithBadType() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public int getId() {return id;}
- public void setId(float id) {}
- """) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
- }
-
- @Test
- fun setterWithAssignableType() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public int getId() {return id;}
- public void setId(Integer id) {}
- """) { _, _ -> }
- .compilesWithoutError()
- }
-
- @Test
- fun getterWithAssignableType() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public Integer getId() {return id;}
- public void setId(int id) {}
- """) { _, _ -> }
- .compilesWithoutError()
- }
-
- @Test
- fun noSetter() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public int getId(){ return id; }
- """) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
- }
-
- @Test
- fun tooManyGetters() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {}
- public int getId(){ return id; }
- public int id(){ return id; }
- """) { _, _ -> }
- .failsToCompile()
- .withErrorContaining("getId, id")
- }
-
- @Test
- fun tooManyGettersWithIgnore() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {}
- public int getId(){ return id; }
- @Ignore public int id(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().getter.name, `is`("getId"))
- }.compilesWithoutError()
- }
-
- @Test
- fun tooManyGettersWithDifferentVisibility() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {}
- public int getId(){ return id; }
- protected int id(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().getter.name, `is`("getId"))
- }.compilesWithoutError()
- }
-
- @Test
- fun tooManyGettersWithDifferentTypes() {
- singleEntity("""
- @PrimaryKey
- public int id;
- public void setId(int id) {}
- public int getId(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().getter.name, `is`("id"))
- assertThat(entity.fields.first().getter.callType, `is`(CallType.FIELD))
- }.compilesWithoutError()
- }
-
- @Test
- fun tooManySetters() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {}
- public void id(int id) {}
- public int getId(){ return id; }
- """) { _, _ -> }
- .failsToCompile()
- .withErrorContaining("setId, id")
- }
-
- @Test
- fun tooManySettersWithIgnore() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {}
- @Ignore public void id(int id) {}
- public int getId(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().setter.name, `is`("setId"))
- }.compilesWithoutError()
- }
-
- @Test
- fun tooManySettersWithDifferentVisibility() {
- singleEntity("""
- @PrimaryKey
- private int id;
- public void setId(int id) {}
- protected void id(int id) {}
- public int getId(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().setter.name, `is`("setId"))
- }.compilesWithoutError()
- }
-
- @Test
- fun tooManySettersWithDifferentTypes() {
- singleEntity("""
- @PrimaryKey
- public int id;
- public void setId(int id) {}
- public int getId(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().setter.name, `is`("id"))
- assertThat(entity.fields.first().setter.callType, `is`(CallType.FIELD))
- }.compilesWithoutError()
- }
-
- @Test
- fun preferPublicOverProtected() {
- singleEntity("""
- @PrimaryKey
- int id;
- public void setId(int id) {}
- public int getId(){ return id; }
- """) { entity, _ ->
- assertThat(entity.fields.first().setter.name, `is`("setId"))
- assertThat(entity.fields.first().getter.name, `is`("getId"))
- }.compilesWithoutError()
- }
-
- @Test
- fun customName() {
- singleEntity("""
- @PrimaryKey
- int x;
- """, hashMapOf(Pair("tableName", "\"foo_table\""))) { entity, _ ->
- assertThat(entity.tableName, `is`("foo_table"))
- }.compilesWithoutError()
- }
-
- @Test
- fun emptyCustomName() {
- singleEntity("""
- @PrimaryKey
- int x;
- """, hashMapOf(Pair("tableName", "\" \""))) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
- }
-
- @Test
- fun missingPrimaryKey() {
- singleEntity("""
- """) { _, _ ->
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.MISSING_PRIMARY_KEY)
- }
-
- @Test
- fun missingColumnAdapter() {
- singleEntity("""
- @PrimaryKey
- public java.util.Date myDate;
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER)
- }
-
- @Test
- fun dropSubPrimaryKey() {
- singleEntity(
- """
- @PrimaryKey
- int id;
- @Embedded
- Point myPoint;
- static class Point {
- @PrimaryKey
- int x;
- int y;
- }
- """
- ) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id")))
- }.compilesWithoutError()
- .withWarningCount(1)
- .withWarningContaining(ProcessorErrors.embeddedPrimaryKeyIsDropped(
- "foo.bar.MyEntity", "x"))
- }
-
- @Test
- fun ignoreDropSubPrimaryKey() {
- singleEntity(
- """
- @PrimaryKey
- int id;
- @Embedded
- @SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED)
- Point myPoint;
- static class Point {
- @PrimaryKey
- int x;
- int y;
- }
- """
- ) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id")))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun notNull() {
- singleEntity(
- """
- @PrimaryKey int id;
- @NonNull public String name;
- """
- ) { entity, _ ->
- val field = fieldsByName(entity, "name").first()
- assertThat(field.name, `is`("name"))
- assertThat(field.columnName, `is`("name"))
- assertThat(field.nonNull, `is`(true))
- }.compilesWithoutError()
- }
-
- private fun fieldsByName(entity: Pojo, vararg fieldNames: String): List<Field> {
- return fieldNames
- .map { name -> entity.fields.find { it.name == name } }
- .filterNotNull()
- }
-
- @Test
- fun index_simple() {
- val annotation = mapOf(
- "indices" to """@Index("foo")"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(name = "index_MyEntity_foo",
- unique = false,
- fields = fieldsByName(entity, "foo")))))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_fromField() {
- singleEntity(
- """
- @PrimaryKey
- public int id;
- @ColumnInfo(index = true)
- public String foo;
- """) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(name = "index_MyEntity_foo",
- unique = false,
- fields = fieldsByName(entity, "foo")))
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_multiColumn() {
- val annotation = mapOf(
- "indices" to """@Index({"foo", "id"})"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(name = "index_MyEntity_foo_id",
- unique = false,
- fields = fieldsByName(entity, "foo", "id")))
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_multiple() {
- val annotation = mapOf(
- "indices" to """{@Index({"foo", "id"}), @Index({"bar_column", "foo"})}"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- @ColumnInfo(name = "bar_column")
- public String bar;
- """
- , annotation) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(name = "index_MyEntity_foo_id",
- unique = false,
- fields = fieldsByName(entity, "foo", "id")),
- Index(name = "index_MyEntity_bar_column_foo",
- unique = false,
- fields = fieldsByName(entity, "bar", "foo")))
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_unique() {
- val annotation = mapOf(
- "indices" to """@Index(value = {"foo", "id"}, unique = true)"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(
- name = "index_MyEntity_foo_id",
- unique = true,
- fields = fieldsByName(entity, "foo", "id")))
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_customName() {
- val annotation = mapOf(
- "indices" to """@Index(value = {"foo"}, name = "myName")"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(name = "myName",
- unique = false,
- fields = fieldsByName(entity, "foo")))
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_customTableName() {
- val annotation = mapOf(
- "tableName" to "\"MyTable\"",
- "indices" to """@Index(value = {"foo"})"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { entity, _ ->
- assertThat(entity.indices, `is`(
- listOf(Index(name = "index_MyTable_foo",
- unique = false,
- fields = fieldsByName(entity, "foo")))
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun index_empty() {
- val annotation = mapOf(
- "indices" to """@Index({})"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY
- )
- }
-
- @Test
- fun index_missingColumn() {
- val annotation = mapOf(
- "indices" to """@Index({"foo", "bar"})"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- public String foo;
- """
- , annotation) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.indexColumnDoesNotExist("bar", listOf("id, foo"))
- )
- }
-
- @Test
- fun index_nameConflict() {
- val annotation = mapOf(
- "indices" to """@Index({"foo"})"""
- )
- singleEntity(
- """
- @PrimaryKey
- public int id;
- @ColumnInfo(index = true)
- public String foo;
- """
- , annotation) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.duplicateIndexInEntity("index_MyEntity_foo")
- )
- }
-
- @Test
- fun index_droppedParentFieldIndex() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- public class Base {
- @PrimaryKey
- long baseId;
- @ColumnInfo(index = true)
- String name;
- String lastName;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """, baseClass = "foo.bar.Base", jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.indices.isEmpty(), `is`(true))
- }.compilesWithoutError()
- .withWarningContaining(
- ProcessorErrors.droppedSuperClassFieldIndex(
- fieldName = "name",
- childEntity = "foo.bar.MyEntity",
- superEntity = "foo.bar.Base")
- )
- }
-
- @Test
- fun index_keptGrandParentEntityIndex() {
- val grandParent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = @Index({"name", "lastName"}))
- public class Base {
- @PrimaryKey
- long baseId;
- String name, lastName;
- }
- """)
- val parent = JavaFileObjects.forSourceLines("foo.bar.Parent",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
-
- public class Parent extends Base {
- String iHaveAField;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Parent",
- attributes = hashMapOf("inheritSuperIndices" to "true"),
- jfos = listOf(parent, grandParent)) {
- entity, _ ->
- assertThat(entity.indices.size, `is`(1))
- assertThat(entity.indices.first(),
- `is`(Index(name = "index_MyEntity_name_lastName",
- unique = false,
- fields = fieldsByName(entity, "name", "lastName"))))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun index_keptParentEntityIndex() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = @Index({"name", "lastName"}))
- public class Base {
- @PrimaryKey
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- attributes = hashMapOf("inheritSuperIndices" to "true"),
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.indices.size, `is`(1))
- assertThat(entity.indices.first(),
- `is`(Index(name = "index_MyEntity_name_lastName",
- unique = false,
- fields = fieldsByName(entity, "name", "lastName"))))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun index_keptParentFieldIndex() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- public class Base {
- @PrimaryKey
- long baseId;
- @ColumnInfo(index = true)
- String name;
- String lastName;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- attributes = hashMapOf("inheritSuperIndices" to "true"),
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.indices.size, `is`(1))
- assertThat(entity.indices.first(),
- `is`(Index(name = "index_MyEntity_name",
- unique = false,
- fields = fieldsByName(entity, "name"))))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun index_droppedGrandParentEntityIndex() {
- val grandParent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = @Index({"name", "lastName"}))
- public class Base {
- @PrimaryKey
- long baseId;
- String name, lastName;
- }
- """)
- val parent = JavaFileObjects.forSourceLines("foo.bar.Parent",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
-
- public class Parent extends Base {
- String iHaveAField;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """, baseClass = "foo.bar.Parent", jfos = listOf(parent, grandParent)) {
- entity, _ ->
- assertThat(entity.indices.isEmpty(), `is`(true))
- }.compilesWithoutError()
- .withWarningContaining(
- ProcessorErrors.droppedSuperClassIndex(
- childEntity = "foo.bar.MyEntity",
- superEntity = "foo.bar.Base")
- )
- }
-
- @Test
- fun index_droppedParentEntityIndex() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(indices = @Index({"name", "lastName"}))
- public class Base {
- @PrimaryKey
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """, baseClass = "foo.bar.Base", jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.indices.isEmpty(), `is`(true))
- }.compilesWithoutError()
- .withWarningContaining(
- ProcessorErrors.droppedSuperClassIndex(
- childEntity = "foo.bar.MyEntity",
- superEntity = "foo.bar.Base")
- )
- }
-
- @Test
- fun index_droppedEmbeddedEntityIndex() {
- singleEntity(
- """
- @PrimaryKey
- public int id;
- @Embedded
- public Foo foo;
- @Entity(indices = {@Index("a")})
- static class Foo {
- @PrimaryKey
- @ColumnInfo(name = "foo_id")
- int id;
- @ColumnInfo(index = true)
- public int a;
- }
- """) { entity, _ ->
- assertThat(entity.indices.isEmpty(), `is`(true))
- }.compilesWithoutError()
- .withWarningContaining(
- ProcessorErrors.droppedEmbeddedIndex(
- entityName = "foo.bar.MyEntity.Foo",
- fieldPath = "foo",
- grandParent = "foo.bar.MyEntity")
- )
- }
-
- @Test
- fun index_onEmbeddedField() {
- singleEntity(
- """
- @PrimaryKey
- public int id;
- @Embedded
- @ColumnInfo(index = true)
- public Foo foo;
- static class Foo {
- @ColumnInfo(index = true)
- public int a;
- }
- """) { entity, _ ->
- assertThat(entity.indices.isEmpty(), `is`(true))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION
- )
- }
-
- @Test
- fun index_droppedEmbeddedFieldIndex() {
- singleEntity(
- """
- @PrimaryKey
- public int id;
- @Embedded
- public Foo foo;
- static class Foo {
- @ColumnInfo(index = true)
- public int a;
- }
- """) { entity, _ ->
- assertThat(entity.indices.isEmpty(), `is`(true))
- }.compilesWithoutError()
- .withWarningContaining(
- ProcessorErrors.droppedEmbeddedFieldIndex("foo > a", "foo.bar.MyEntity")
- )
- }
-
- @Test
- fun index_referenceEmbeddedField() {
- singleEntity(
- """
- @PrimaryKey
- public int id;
- @Embedded
- public Foo foo;
- static class Foo {
- public int a;
- }
- """, attributes = mapOf("indices" to "@Index(\"a\")")) { entity, _ ->
- assertThat(entity.indices.size, `is`(1))
- assertThat(entity.indices.first(), `is`(
- Index(
- name = "index_MyEntity_a",
- unique = false,
- fields = fieldsByName(entity, "a")
- )
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun primaryKey_definedInBothWays() {
- singleEntity(
- """
- public int id;
- @PrimaryKey
- public String foo;
- """,
- attributes = mapOf("primaryKeys" to "\"id\"")) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.multiplePrimaryKeyAnnotations(
- listOf("PrimaryKey[id]", "PrimaryKey[foo]")
- ))
- }
-
- @Test
- fun primaryKey_badColumnName() {
- singleEntity(
- """
- public int id;
- """,
- attributes = mapOf("primaryKeys" to "\"foo\"")) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.primaryKeyColumnDoesNotExist("foo", listOf("id")))
- }
-
- @Test
- fun primaryKey_multipleAnnotations() {
- singleEntity("""
- @PrimaryKey
- int x;
- @PrimaryKey
- int y;
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.isEmpty(), `is`(true))
- }.failsToCompile()
- .withErrorContaining(
- ProcessorErrors.multiplePrimaryKeyAnnotations(
- listOf("PrimaryKey[x]", "PrimaryKey[y]")))
- }
-
- @Test
- fun primaryKey_fromParentField() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- public class Base {
- @PrimaryKey
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("baseId"))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun primaryKey_fromParentEntity() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(primaryKeys = "baseId")
- public class Base {
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("baseId"))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun primaryKey_overrideFromParentField() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- public class Base {
- @PrimaryKey
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- assertThat(entity.primaryKey.autoGenerateId, `is`(false))
- }.compilesWithoutError().withNoteContaining(
- "PrimaryKey[baseId] is overridden by PrimaryKey[id]"
- )
- }
-
- @Test
- fun primaryKey_overrideFromParentEntityViaField() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(primaryKeys = "baseId")
- public class Base {
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- }.compilesWithoutError().withNoteContaining(
- "PrimaryKey[baseId] is overridden by PrimaryKey[id]"
- )
- }
-
- @Test
- fun primaryKey_overrideFromParentEntityViaEntity() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity(primaryKeys = "baseId")
- public class Base {
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent),
- attributes = mapOf("primaryKeys" to "\"id\"")) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- assertThat(entity.primaryKey.autoGenerateId, `is`(false))
- }.compilesWithoutError().withNoteContaining(
- "PrimaryKey[baseId] is overridden by PrimaryKey[id]"
- )
- }
-
- @Test
- fun primaryKey_autoGenerate() {
- listOf("long", "Long", "Integer", "int").forEach { type ->
- singleEntity(
- """
- @PrimaryKey(autoGenerate = true)
- public $type id;
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- assertThat(entity.primaryKey.autoGenerateId, `is`(true))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun primaryKey_nonNull_notNeeded() {
- listOf("long", "Long", "Integer", "int").forEach { type ->
- singleEntity(
- """
- @PrimaryKey
- public $type id;
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- assertThat(entity.primaryKey.autoGenerateId, `is`(false))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun primaryKey_autoGenerateBadType() {
- listOf("String", "float", "Float", "Double", "double").forEach { type ->
- singleEntity(
- """
- @PrimaryKey(autoGenerate = true)
- public $type id;
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- assertThat(entity.primaryKey.autoGenerateId, `is`(true))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT)
- }
- }
-
- @Test
- fun primaryKey_embedded() {
- singleEntity(
- """
- public int id;
-
- @Embedded(prefix = "bar_")
- @PrimaryKey
- @NonNull
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.columnName },
- `is`(listOf("bar_a", "bar_b")))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun primaryKey_embeddedInherited() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
-
- public class Base {
- long baseId;
- String name, lastName;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- @NonNull
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- }
- """)
- singleEntity(
- """
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.columnName },
- `is`(listOf("bar_a", "bar_b")))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun primaryKey_overrideViaEmbedded() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
-
- @Entity(primaryKeys = "baseId")
- public class Base {
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- public int id;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- @NonNull
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.columnName },
- `is`(listOf("bar_a", "bar_b")))
- }.compilesWithoutError().withNoteContaining(
- "PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]")
- }
-
- @Test
- fun primaryKey_overrideEmbedded() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
-
- public class Base {
- long baseId;
- String name, lastName;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- @NonNull
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.columnName },
- `is`(listOf("id")))
- }.compilesWithoutError().withNoteContaining(
- "PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
- }
-
- @Test
- fun primaryKey_NonNull() {
- singleEntity(
- """
- @PrimaryKey
- @NonNull
- public String id;
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- }.compilesWithoutError()
- }
-
- @Test
- fun primaryKey_Nullable() {
- singleEntity(
- """
- @PrimaryKey
- public String id;
- """) { entity, _ ->
- assertThat(entity.primaryKey.fields.size, `is`(1))
- assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("id"))
- }
-
- @Test
- fun primaryKey_MultipleNullable() {
- singleEntity(
- """
- @PrimaryKey
- public String id;
- @PrimaryKey
- public String anotherId;
- """) { entity, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("id"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("anotherId"))
- }
-
- @Test
- fun primaryKey_MultipleNullableAndNonNullable() {
- singleEntity(
- """
- @PrimaryKey
- @NonNull
- public String id;
- @PrimaryKey
- public String anotherId;
- """) { entity, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("anotherId"))
- }
-
- @Test
- fun primaryKey_definedAsAttributesNullable() {
- singleEntity(
- """
- public int id;
- public String foo;
- """,
- attributes = mapOf("primaryKeys" to "{\"id\", \"foo\"}")) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- }
-
- @Test
- fun primaryKey_definedAsAttributesNonNull() {
- singleEntity(
- """
- public int id;
- @NonNull
- public String foo;
- """,
- attributes = mapOf("primaryKeys" to "{\"id\", \"foo\"}")) { entity, _ ->
- assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id", "foo")))
- }.compilesWithoutError()
- }
-
- @Test
- fun primaryKey_nullableEmbedded() {
- singleEntity(
- """
- public int id;
-
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- }
-
- @Test
- fun primaryKey_nullableEmbeddedObject() {
- singleEntity(
- """
- public int id;
-
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public String a;
- public String b;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- .and().withErrorCount(3)
- }
-
- @Test
- fun primaryKey_nullableEmbeddedObject_multipleParents() {
- singleEntity(
- """
- public int id;
-
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- @Embedded(prefix = "baz_")
- public Baz a;
- public String b;
-
- static class Baz {
- public Integer bb;
- }
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a > bb"))
- .and().withErrorCount(4)
- }
-
- @Test
- fun primaryKey_nullableEmbeddedInherited() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
-
- public class Base {
- long baseId;
- String name, lastName;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- }
- """)
- singleEntity(
- """
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
- .and().withErrorCount(3)
- }
-
- @Test
- fun primaryKey_nullableOverrideViaEmbedded() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
-
- @Entity(primaryKeys = "baseId")
- public class Base {
- long baseId;
- String name, lastName;
- }
- """)
- singleEntity(
- """
- public int id;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
- .and().withNoteContaining(
- "PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]")
- .and().withErrorCount(3)
- }
-
- @Test
- fun primaryKey_nullableOverrideEmbedded() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
-
- public class Base {
- long baseId;
- String name, lastName;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public int a;
- public int b;
- }
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
- .and().withNoteContaining(
- "PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
- .and().withErrorCount(3)
- }
-
- @Test
- fun primaryKey_integerOverrideEmbedded() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
-
- public class Base {
- long baseId;
- String name, lastName;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public Integer a;
- }
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { _, _ ->
- }.compilesWithoutError().withNoteContaining(
- "PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
- }
-
- @Test
- fun primaryKey_singleStringPrimaryKeyOverrideEmbedded() {
- val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
- """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
-
- public class Base {
- long baseId;
- String name, lastName;
- @Embedded(prefix = "bar_")
- @PrimaryKey
- public Foo foo;
-
- static class Foo {
- public String a;
- }
- }
- """)
- singleEntity(
- """
- @PrimaryKey
- public int id;
- """,
- baseClass = "foo.bar.Base",
- jfos = listOf(parent)) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
- .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
- .and().withNoteContaining(
- "PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
- .and().withErrorCount(2)
- }
-
- @Test
- fun relationInEntity() {
- singleEntity(
- """
- @PrimaryKey
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- java.util.List<User> users;
- """, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(RELATION_IN_ENTITY)
- }
-
- @Test
- fun foreignKey_invalidAction() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name",
- onDelete = 101
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
- }
-
- @Test
- fun foreignKey_badEntity() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = dsa.class,
- parentColumns = "lastName",
- childColumns = "name"
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining("cannot find symbol")
- }
-
- @Test
- fun foreignKey_notAnEntity() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.NOT_AN_ENTITY_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name"
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.NOT_AN_ENTITY)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.foreignKeyNotAnEntity(
- COMMON.NOT_AN_ENTITY_TYPE_NAME.toString()))
- }
-
- @Test
- fun foreignKey_invalidChildColumn() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "namex"
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.foreignKeyChildColumnDoesNotExist(
- "namex", listOf("id", "name")))
- }
-
- @Test
- fun foreignKey_columnCountMismatch() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = {"name", "id"}
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.foreignKeyColumnNumberMismatch(
- listOf("name", "id"), listOf("lastName")))
- }
-
- @Test
- fun foreignKey_emptyChildColumns() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = {}
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
- }
-
- @Test
- fun foreignKey_emptyParentColumns() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = {},
- childColumns = {"name"}
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
- }
-
- @Test
- fun foreignKey_simple() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name",
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE,
- deferred = true
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { entity, _ ->
- assertThat(entity.foreignKeys.size, `is`(1))
- val fKey = entity.foreignKeys.first()
- assertThat(fKey.parentTable, `is`("User"))
- assertThat(fKey.parentColumns, `is`(listOf("lastName")))
- assertThat(fKey.deferred, `is`(true))
- assertThat(fKey.childFields.size, `is`(1))
- val field = fKey.childFields.first()
- assertThat(field.name, `is`("name"))
- }.compilesWithoutError()
- }
-
- @Test
- fun foreignKey_dontDuplicationChildIndex_SingleColumn() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name",
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE,
- deferred = true
- )}""".trimIndent(),
- "indices" to """@Index("name")"""
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.compilesWithoutWarnings()
- }
-
- @Test
- fun foreignKey_dontDuplicationChildIndex_MultipleColumns() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = {"lastName", "name"},
- childColumns = {"lName", "name"},
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE,
- deferred = true
- )}""".trimIndent(),
- "indices" to """@Index({"lName", "name"})"""
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- String lName;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { entity, _ ->
- assertThat(entity.indices.size, `is`(1))
- }.compilesWithoutWarnings()
- }
-
- @Test
- fun foreignKey_dontDuplicationChildIndex_WhenCovered() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = {"lastName"},
- childColumns = {"name"},
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE,
- deferred = true
- )}""".trimIndent(),
- "indices" to """@Index({"name", "lName"})"""
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- String lName;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { entity, _ ->
- assertThat(entity.indices.size, `is`(1))
- }.compilesWithoutWarnings()
- }
-
- @Test
- fun foreignKey_warnMissingChildIndex() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "name",
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE,
- deferred = true
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { entity, _ ->
- assertThat(entity.indices, `is`(emptyList()))
- }.compilesWithoutError().withWarningContaining(
- ProcessorErrors.foreignKeyMissingIndexInChildColumn("name"))
- }
-
- @Test
- fun foreignKey_warnMissingChildrenIndex() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = {"lastName", "name"},
- childColumns = {"lName", "name"}
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- String lName;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { entity, _ ->
- assertThat(entity.indices, `is`(emptyList()))
- }.compilesWithoutError().withWarningContaining(
- ProcessorErrors.foreignKeyMissingIndexInChildColumns(listOf("lName", "name")))
- }
-
- @Test
- fun foreignKey_dontIndexIfAlreadyPrimaryKey() {
- val annotation = mapOf(
- "foreignKeys" to """{@ForeignKey(
- entity = ${COMMON.USER_TYPE_NAME}.class,
- parentColumns = "lastName",
- childColumns = "id",
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE,
- deferred = true
- )}""".trimIndent()
- )
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { entity, _ ->
- assertThat(entity.indices, `is`(emptyList()))
- }.compilesWithoutWarnings()
- }
-
- @Test
- fun recursion_1Level() {
- singleEntity(
- """
- @Embedded
- MyEntity myEntity;
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyEntity -> foo.bar.MyEntity"))
- }
-
- @Test
- fun recursion_2Levels_embedToRelation() {
- singleEntity(
- """
- int pojoId;
- @Embedded
- A a;
-
- static class A {
- int entityId;
- @Relation(parentColumn = "pojoId", entityColumn = "entityId")
- MyEntity myEntity;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
- }
-
- @Test
- fun recursion_2Levels_onlyEmbeds_entityToPojo() {
- singleEntity(
- """
- @Embedded
- A a;
-
- static class A {
- @Embedded
- MyEntity myEntity;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
- }
-
- @Test
- fun recursion_2Levels_onlyEmbeds_onlyEntities() {
- singleEntity(
- """
- @Embedded
- A a;
-
- @Entity
- static class A {
- @Embedded
- MyEntity myEntity;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
- }
- fun okTableName() {
- val annotation = mapOf("tableName" to "\"foo bar\"")
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.compilesWithoutError()
- }
-
- @Test
- fun badTableName() {
- val annotation = mapOf("tableName" to """ "foo`bar" """)
- singleEntity(
- """
- @PrimaryKey
- int id;
- String name;
- """,
- attributes = annotation, jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_TABLE_NAME)
- }
-
- @Test
- fun badColumnName() {
- singleEntity(
- """
- @PrimaryKey
- int id;
- @ColumnInfo(name = "\"foo bar\"")
- String name;
- """,
- jfos = listOf(COMMON.USER)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_COLUMN_NAME)
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/FieldProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/FieldProcessorTest.kt
deleted file mode 100644
index ef0170c..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/FieldProcessorTest.kt
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.parser.Collate
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.solver.types.ColumnTypeAdapter
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.Field
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import simpleRun
-import javax.lang.model.element.Element
-import javax.lang.model.element.ElementKind
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-
-@Suppress("HasPlatformType")
-@RunWith(JUnit4::class)
-class FieldProcessorTest {
- companion object {
- const val ENTITY_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- abstract class MyEntity {
- """
- const val ENTITY_SUFFIX = "}"
- val ALL_PRIMITIVES = arrayListOf(
- TypeKind.INT,
- TypeKind.BYTE,
- TypeKind.SHORT,
- TypeKind.LONG,
- TypeKind.CHAR,
- TypeKind.FLOAT,
- TypeKind.DOUBLE)
- val ARRAY_CONVERTER = JavaFileObjects.forSourceLines("foo.bar.MyConverter",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- public class MyConverter {
- ${ALL_PRIMITIVES.joinToString("\n") {
- val arrayDef = "${it.name.toLowerCase()}[]"
- "@TypeConverter public static String" +
- " arrayIntoString($arrayDef input) { return null;}" +
- "@TypeConverter public static $arrayDef" +
- " stringIntoArray${it.name}(String input) { return null;}"
- }}
- ${ALL_PRIMITIVES.joinToString("\n") {
- val arrayDef = "${it.box()}[]"
- "@TypeConverter public static String" +
- " arrayIntoString($arrayDef input) { return null;}" +
- "@TypeConverter public static $arrayDef" +
- " stringIntoArray${it.name}Boxed(String input) { return null;}"
- }}
- }
- """)
-
- private fun TypeKind.box(): String {
- return "java.lang." + when (this) {
- TypeKind.INT -> "Integer"
- TypeKind.CHAR -> "Character"
- else -> this.name.toLowerCase().capitalize()
- }
- }
-
- // these 2 box methods are ugly but makes tests nicer and they are private
- private fun TypeKind.typeMirror(invocation: TestInvocation): TypeMirror {
- return invocation.processingEnv.typeUtils.getPrimitiveType(this)
- }
-
- private fun TypeKind.affinity(): SQLTypeAffinity {
- return when (this) {
- TypeKind.FLOAT, TypeKind.DOUBLE -> SQLTypeAffinity.REAL
- else -> SQLTypeAffinity.INTEGER
- }
- }
-
- private fun TypeKind.box(invocation: TestInvocation): TypeMirror {
- return invocation.processingEnv.elementUtils.getTypeElement(box()).asType()
- }
- }
-
- @Test
- fun primitives() {
- ALL_PRIMITIVES.forEach { primitive ->
- singleEntity("${primitive.name.toLowerCase()} x;") { field, invocation ->
- assertThat(field, `is`(
- Field(name = "x",
- type = primitive.typeMirror(invocation),
- element = field.element,
- affinity = primitive.affinity()
- )))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun boxed() {
- ALL_PRIMITIVES.forEach { primitive ->
- singleEntity("${primitive.box()} y;") { field, invocation ->
- assertThat(field, `is`(
- Field(name = "y",
- type = primitive.box(invocation),
- element = field.element,
- affinity = primitive.affinity())))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun columnName() {
- singleEntity("""
- @ColumnInfo(name = "foo")
- @PrimaryKey
- int x;
- """) { field, invocation ->
- assertThat(field, `is`(
- Field(name = "x",
- type = TypeKind.INT.typeMirror(invocation),
- element = field.element,
- columnName = "foo",
- affinity = SQLTypeAffinity.INTEGER)))
- }.compilesWithoutError()
- }
-
- @Test
- fun indexed() {
- singleEntity("""
- @ColumnInfo(name = "foo", index = true)
- int x;
- """) { field, invocation ->
- assertThat(field, `is`(
- Field(name = "x",
- type = TypeKind.INT.typeMirror(invocation),
- element = field.element,
- columnName = "foo",
- affinity = SQLTypeAffinity.INTEGER,
- indexed = true)))
- }.compilesWithoutError()
- }
-
- @Test
- fun emptyColumnName() {
- singleEntity("""
- @ColumnInfo(name = "")
- int x;
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY)
- }
-
- @Test
- fun byteArrayWithEnforcedType() {
- singleEntity("@TypeConverters(foo.bar.MyConverter.class)" +
- "@ColumnInfo(typeAffinity = ColumnInfo.TEXT) byte[] arr;") { field, invocation ->
- assertThat(field, `is`(Field(name = "arr",
- type = invocation.processingEnv.typeUtils.getArrayType(
- TypeKind.BYTE.typeMirror(invocation)),
- element = field.element,
- affinity = SQLTypeAffinity.TEXT)))
- assertThat((field.cursorValueReader as? ColumnTypeAdapter)?.typeAffinity,
- `is`(SQLTypeAffinity.TEXT))
- }.compilesWithoutError()
- }
-
- @Test
- fun primitiveArray() {
- ALL_PRIMITIVES.forEach { primitive ->
- singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
- "${primitive.toString().toLowerCase()}[] arr;") { field, invocation ->
- assertThat(field, `is`(
- Field(name = "arr",
- type = invocation.processingEnv.typeUtils.getArrayType(
- primitive.typeMirror(invocation)),
- element = field.element,
- affinity = if (primitive == TypeKind.BYTE) {
- SQLTypeAffinity.BLOB
- } else {
- SQLTypeAffinity.TEXT
- })))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun boxedArray() {
- ALL_PRIMITIVES.forEach { primitive ->
- singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
- "${primitive.box()}[] arr;") { field, invocation ->
- assertThat(field, `is`(
- Field(name = "arr",
- type = invocation.processingEnv.typeUtils.getArrayType(
- primitive.box(invocation)),
- element = field.element,
- affinity = SQLTypeAffinity.TEXT)))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun generic() {
- singleEntity("""
- static class BaseClass<T> {
- T item;
- }
- @Entity
- static class Extending extends BaseClass<java.lang.Integer> {
- }
- """) { field, invocation ->
- assertThat(field, `is`(Field(name = "item",
- type = TypeKind.INT.box(invocation),
- element = field.element,
- affinity = SQLTypeAffinity.INTEGER)))
- }.compilesWithoutError()
- }
-
- @Test
- fun unboundGeneric() {
- singleEntity("""
- @Entity
- static class BaseClass<T> {
- T item;
- }
- """) { _, _ -> }.failsToCompile()
- .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
- }
-
- @Test
- fun nameVariations() {
- simpleRun {
- assertThat(Field(mock(Element::class.java), "x", TypeKind.INT.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
- assertThat(Field(mock(Element::class.java), "x", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
- assertThat(Field(mock(Element::class.java), "xAll",
- TypeKind.BOOLEAN.typeMirror(it), SQLTypeAffinity.INTEGER)
- .nameWithVariations, `is`(arrayListOf("xAll")))
- }
- }
-
- @Test
- fun nameVariations_is() {
- val elm = mock(Element::class.java)
- simpleRun {
- assertThat(Field(elm, "isX", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX", "x")))
- assertThat(Field(elm, "isX", TypeKind.INT.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX")))
- assertThat(Field(elm, "is", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("is")))
- assertThat(Field(elm, "isAllItems", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations,
- `is`(arrayListOf("isAllItems", "allItems")))
- }
- }
-
- @Test
- fun nameVariations_has() {
- val elm = mock(Element::class.java)
- simpleRun {
- assertThat(Field(elm, "hasX", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX", "x")))
- assertThat(Field(elm, "hasX", TypeKind.INT.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX")))
- assertThat(Field(elm, "has", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("has")))
- assertThat(Field(elm, "hasAllItems", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations,
- `is`(arrayListOf("hasAllItems", "allItems")))
- }
- }
-
- @Test
- fun nameVariations_m() {
- val elm = mock(Element::class.java)
- simpleRun {
- assertThat(Field(elm, "mall", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mall")))
- assertThat(Field(elm, "mallVars", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mallVars")))
- assertThat(Field(elm, "mAll", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mAll", "all")))
- assertThat(Field(elm, "m", TypeKind.INT.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("m")))
- assertThat(Field(elm, "mallItems", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations,
- `is`(arrayListOf("mallItems")))
- assertThat(Field(elm, "mAllItems", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations,
- `is`(arrayListOf("mAllItems", "allItems")))
- }
- }
-
- @Test
- fun nameVariations_underscore() {
- val elm = mock(Element::class.java)
- simpleRun {
- assertThat(Field(elm, "_all", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_all", "all")))
- assertThat(Field(elm, "_", TypeKind.INT.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_")))
- assertThat(Field(elm, "_allItems", TypeKind.BOOLEAN.typeMirror(it),
- SQLTypeAffinity.INTEGER).nameWithVariations,
- `is`(arrayListOf("_allItems", "allItems")))
- }
- }
-
- @Test
- fun collate() {
- Collate.values().forEach { collate ->
- singleEntity("""
- @PrimaryKey
- @ColumnInfo(collate = ColumnInfo.${collate.name})
- String code;
- """) { field, invocation ->
- assertThat(field, `is`(
- Field(name = "code",
- type = invocation.context.COMMON_TYPES.STRING,
- element = field.element,
- columnName = "code",
- collate = collate,
- affinity = SQLTypeAffinity.TEXT)))
- }.compilesWithoutError()
- }
- }
-
- fun singleEntity(vararg input: String, handler: (Field, invocation: TestInvocation) -> Unit):
- CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
- ENTITY_PREFIX + input.joinToString("\n") + ENTITY_SUFFIX
- ), ARRAY_CONVERTER))
- .processedWith(TestProcessor.builder()
- .forAnnotations(android.arch.persistence.room.Entity::class)
- .nextRunHandler { invocation ->
- val (owner, field) = invocation.roundEnv
- .getElementsAnnotatedWith(Entity::class.java)
- .map {
- Pair(it, invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .firstOrNull { it.kind == ElementKind.FIELD })
- }
- .first { it.second != null }
- val entityContext =
- EntityProcessor(invocation.context, MoreElements.asType(owner))
- .context
- val parser = FieldProcessor(
- baseContext = entityContext,
- containing = MoreTypes.asDeclared(owner.asType()),
- element = field!!,
- bindingScope = FieldProcessor.BindingScope.TWO_WAY,
- fieldParent = null)
- handler(parser.process(), invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/InsertionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/InsertionMethodProcessorTest.kt
deleted file mode 100644
index 51f81db..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/InsertionMethodProcessorTest.kt
+++ /dev/null
@@ -1,423 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import COMMON
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.OnConflictStrategy
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.InsertionMethod
-import android.arch.persistence.room.vo.InsertionMethod.Type
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth.assertAbout
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-@RunWith(JUnit4::class)
-class InsertionMethodProcessorTest {
- companion object {
- const val DAO_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import java.util.*;
- @Dao
- abstract class MyClass {
- """
- const val DAO_SUFFIX = "}"
- val USER_TYPE_NAME: TypeName = COMMON.USER_TYPE_NAME
- val BOOK_TYPE_NAME: TypeName = ClassName.get("foo.bar", "Book")
- }
-
- @Test
- fun readNoParams() {
- singleInsertMethod(
- """
- @Insert
- abstract public void foo();
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("foo"))
- assertThat(insertion.parameters.size, `is`(0))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- assertThat(insertion.entities.size, `is`(0))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT)
- }
-
- @Test
- fun insertSingle() {
- singleInsertMethod(
- """
- @Insert
- abstract public long foo(User user);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("foo"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(USER_TYPE_NAME))
- assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
- assertThat(insertion.entities["user"]?.typeName,
- `is`(ClassName.get("foo.bar", "User") as TypeName))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.LONG))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertNotAnEntity() {
- singleInsertMethod(
- """
- @Insert
- abstract public void foo(NotAnEntity notValid);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("foo"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.entityType, `is`(nullValue()))
- assertThat(insertion.entities.size, `is`(0))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
- )
- }
-
- @Test
- fun insertTwo() {
- singleInsertMethod(
- """
- @Insert
- abstract public void foo(User u1, User u2);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("foo"))
-
- assertThat(insertion.parameters.size, `is`(2))
- insertion.parameters.forEach {
- assertThat(it.type.typeName(), `is`(USER_TYPE_NAME))
- assertThat(it.entityType?.typeName(), `is`(USER_TYPE_NAME))
- }
- assertThat(insertion.entities.size, `is`(2))
- assertThat(insertion.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.entities["u2"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.parameters.map { it.name }, `is`(listOf("u1", "u2")))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertList() {
- singleInsertMethod(
- """
- @Insert
- abstract public List<Long> insertUsers(List<User> users);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("insertUsers"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(
- ClassName.get("java.util", "List"), USER_TYPE_NAME) as TypeName))
- assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
- assertThat(insertion.entities.size, `is`(1))
- assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.returnType.typeName(), `is`(
- ParameterizedTypeName.get(ClassName.get("java.util", "List"),
- ClassName.get("java.lang", "Long")) as TypeName
- ))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertArray() {
- singleInsertMethod(
- """
- @Insert
- abstract public void insertUsers(User[] users);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("insertUsers"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ArrayTypeName.of(COMMON.USER_TYPE_NAME) as TypeName))
- assertThat(insertion.entities.size, `is`(1))
- assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertSet() {
- singleInsertMethod(
- """
- @Insert
- abstract public void insertUsers(Set<User> users);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("insertUsers"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(ClassName.get("java.util", "Set")
- , COMMON.USER_TYPE_NAME) as TypeName))
- assertThat(insertion.entities.size, `is`(1))
- assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertQueue() {
- singleInsertMethod(
- """
- @Insert
- abstract public void insertUsers(Queue<User> users);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("insertUsers"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(ClassName.get("java.util", "Queue")
- , USER_TYPE_NAME) as TypeName))
- assertThat(insertion.entities.size, `is`(1))
- assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertIterable() {
- singleInsertMethod("""
- @Insert
- abstract public void insert(Iterable<User> users);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("insert"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(ParameterizedTypeName.get(
- ClassName.get("java.lang", "Iterable"), USER_TYPE_NAME) as TypeName))
- assertThat(insertion.entities.size, `is`(1))
- assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertCustomCollection() {
- singleInsertMethod("""
- static class MyList<Irrelevant, Item> extends ArrayList<Item> {}
- @Insert
- abstract public void insert(MyList<String, User> users);
- """) { insertion, _ ->
- assertThat(insertion.name, `is`("insert"))
- assertThat(insertion.parameters.size, `is`(1))
- val param = insertion.parameters.first()
- assertThat(param.type.typeName(), `is`(ParameterizedTypeName.get(
- ClassName.get("foo.bar", "MyClass.MyList"),
- CommonTypeNames.STRING, USER_TYPE_NAME) as TypeName))
- assertThat(insertion.entities.size, `is`(1))
- assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun insertDifferentTypes() {
- singleInsertMethod(
- """
- @Insert
- abstract public void foo(User u1, Book b1);
- """) { insertion, _ ->
- assertThat(insertion.parameters.size, `is`(2))
- assertThat(insertion.parameters[0].type.typeName().toString(),
- `is`("foo.bar.User"))
- assertThat(insertion.parameters[1].type.typeName().toString(),
- `is`("foo.bar.Book"))
- assertThat(insertion.parameters.map { it.name }, `is`(listOf("u1", "b1")))
- assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
- assertThat(insertion.entities.size, `is`(2))
- assertThat(insertion.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(insertion.entities["b1"]?.typeName, `is`(BOOK_TYPE_NAME))
- }.compilesWithoutError()
- }
-
- @Test
- fun onConflict_Default() {
- singleInsertMethod(
- """
- @Insert
- abstract public void foo(User user);
- """) { insertion, _ ->
- assertThat(insertion.onConflict, `is`(OnConflictStrategy.ABORT))
- }.compilesWithoutError()
- }
-
- @Test
- fun onConflict_Invalid() {
- singleInsertMethod(
- """
- @Insert(onConflict = -1)
- abstract public void foo(User user);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
- }
-
- @Test
- fun onConflict_EachValue() {
- listOf(
- Pair("REPLACE", 1),
- Pair("ROLLBACK", 2),
- Pair("ABORT", 3),
- Pair("FAIL", 4),
- Pair("IGNORE", 5)
- ).forEach { pair ->
- singleInsertMethod(
- """
- @Insert(onConflict=OnConflictStrategy.${pair.first})
- abstract public void foo(User user);
- """) { insertion, _ ->
- assertThat(insertion.onConflict, `is`(pair.second))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun invalidReturnType() {
- singleInsertMethod(
- """
- @Insert
- abstract public int foo(User user);
- """) { insertion, _ ->
- assertThat(insertion.insertionType, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.INVALID_INSERTION_METHOD_RETURN_TYPE)
- }
-
- @Test
- fun mismatchedReturnType() {
- singleInsertMethod(
- """
- @Insert
- abstract public long[] foo(User user);
- """) { insertion, _ ->
- assertThat(insertion.insertionType, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.insertionMethodReturnTypeMismatch(
- ArrayTypeName.of(TypeName.LONG),
- InsertionMethodProcessor.SINGLE_ITEM_SET.map { it.returnTypeName }))
- }
-
- @Test
- fun mismatchedReturnType2() {
- singleInsertMethod(
- """
- @Insert
- abstract public long foo(User... user);
- """) { insertion, _ ->
- assertThat(insertion.insertionType, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.insertionMethodReturnTypeMismatch(
- TypeName.LONG,
- InsertionMethodProcessor.MULTIPLE_ITEM_SET.map { it.returnTypeName }))
- }
-
- @Test
- fun mismatchedReturnType3() {
- singleInsertMethod(
- """
- @Insert
- abstract public long foo(User user1, User user2);
- """) { insertion, _ ->
- assertThat(insertion.insertionType, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.insertionMethodReturnTypeMismatch(
- TypeName.LONG,
- InsertionMethodProcessor.VOID_SET.map { it.returnTypeName }))
- }
-
- @Test
- fun validReturnTypes() {
- listOf(
- Pair("void", Type.INSERT_VOID),
- Pair("long", Type.INSERT_SINGLE_ID),
- Pair("long[]", Type.INSERT_ID_ARRAY),
- Pair("Long[]", Type.INSERT_ID_ARRAY_BOX),
- Pair("List<Long>", Type.INSERT_ID_LIST)
- ).forEach { pair ->
- val dots = if (pair.second in setOf(Type.INSERT_ID_LIST, Type.INSERT_ID_ARRAY,
- Type.INSERT_ID_ARRAY_BOX)) {
- "..."
- } else {
- ""
- }
- singleInsertMethod(
- """
- @Insert
- abstract public ${pair.first} foo(User$dots user);
- """) { insertion, _ ->
- assertThat(insertion.insertMethodTypeFor(insertion.parameters.first()),
- `is`(pair.second))
- assertThat(pair.toString(), insertion.insertionType, `is`(pair.second))
- }.compilesWithoutError()
- }
- }
-
- fun singleInsertMethod(
- vararg input: String,
- handler: (InsertionMethod, TestInvocation) -> Unit
- ): CompileTester {
- return assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
- DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
- ), COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY))
- .processedWith(TestProcessor.builder()
- .forAnnotations(Insert::class, Dao::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.java)
- .map {
- Pair(it,
- invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .filter {
- MoreElements.isAnnotationPresent(it,
- Insert::class.java)
- }
- )
- }.filter { it.second.isNotEmpty() }.first()
- val processor = InsertionMethodProcessor(
- baseContext = invocation.context,
- containing = MoreTypes.asDeclared(owner.asType()),
- executableElement = MoreElements.asExecutable(methods.first()))
- val processed = processor.process()
- handler(processed, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/PojoProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/PojoProcessorTest.kt
deleted file mode 100644
index c12424d..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/PojoProcessorTest.kt
+++ /dev/null
@@ -1,958 +0,0 @@
-/*
- * 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.processor
-
-import COMMON
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_TYPE
-import android.arch.persistence.room.processor.ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
-import android.arch.persistence.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
-import android.arch.persistence.room.processor.ProcessorErrors.RELATION_NOT_COLLECTION
-import android.arch.persistence.room.processor.ProcessorErrors.relationCannotFindEntityField
-import android.arch.persistence.room.processor.ProcessorErrors.relationCannotFindParentEntityField
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.vo.CallType
-import android.arch.persistence.room.vo.Constructor
-import android.arch.persistence.room.vo.EmbeddedField
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.Pojo
-import android.arch.persistence.room.vo.RelationCollector
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeName
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.instanceOf
-import org.hamcrest.CoreMatchers.not
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.CoreMatchers.sameInstance
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import simpleRun
-import javax.lang.model.element.Element
-import javax.tools.JavaFileObject
-
-/**
- * Some of the functionality is tested via EntityProcessor.
- */
-@RunWith(JUnit4::class)
-class PojoProcessorTest {
-
- companion object {
- val MY_POJO: ClassName = ClassName.get("foo.bar", "MyPojo")
- val HEADER = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import java.util.*;
- public class MyPojo {
- """
- val FOOTER = "\n}"
- }
-
- private fun String.toJFO(qName: String) = JavaFileObjects.forSourceLines(qName, this)
-
- @Test
- fun inheritedPrivate() {
- val parent = """
- package foo.bar.x;
- import android.arch.persistence.room.*;
- public class BaseClass {
- private String baseField;
- public String getBaseField(){ return baseField; }
- public void setBaseField(String baseField){ }
- }
- """
- simpleRun(
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- public class ${MY_POJO.simpleName()} extends foo.bar.x.BaseClass {
- public String myField;
- }
- """.toJFO(MY_POJO.toString()),
- parent.toJFO("foo.bar.x.BaseClass")) { invocation ->
- val pojo = PojoProcessor(baseContext = invocation.context,
- element = invocation.typeElement(MY_POJO.toString()),
- bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
- parent = null).process()
- assertThat(pojo.fields.find { it.name == "myField" }, notNullValue())
- assertThat(pojo.fields.find { it.name == "baseField" }, notNullValue())
- }.compilesWithoutError()
- }
-
- @Test
- fun transient_ignore() {
- singleRun("""
- transient int foo;
- int bar;
- """) { pojo ->
- assertThat(pojo.fields.size, `is`(1))
- assertThat(pojo.fields[0].name, `is`("bar"))
- }.compilesWithoutError()
- }
-
- @Test
- fun transient_withColumnInfo() {
- singleRun("""
- @ColumnInfo
- transient int foo;
- int bar;
- """) { pojo ->
- assertThat(pojo.fields.map { it.name }.toSet(), `is`(setOf("bar", "foo")))
- }.compilesWithoutError()
- }
-
- @Test
- fun transient_embedded() {
- singleRun("""
- @Embedded
- transient Foo foo;
- int bar;
- static class Foo {
- int x;
- }
- """) { pojo ->
- assertThat(pojo.fields.map { it.name }.toSet(), `is`(setOf("x", "bar")))
- }.compilesWithoutError()
- }
-
- @Test
- fun transient_insideEmbedded() {
- singleRun("""
- @Embedded
- Foo foo;
- int bar;
- static class Foo {
- transient int x;
- int y;
- }
- """) { pojo ->
- assertThat(pojo.fields.map { it.name }.toSet(), `is`(setOf("bar", "y")))
- }.compilesWithoutError()
- }
-
- @Test
- fun transient_relation() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- public transient List<User> user;
- """, COMMON.USER
- ) { pojo ->
- assertThat(pojo.relations.size, `is`(1))
- assertThat(pojo.relations.first().entityField.name, `is`("uid"))
- assertThat(pojo.relations.first().parentField.name, `is`("id"))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun embedded() {
- singleRun(
- """
- int id;
- @Embedded
- Point myPoint;
- static class Point {
- int x;
- int y;
- }
- """
- ) { pojo ->
- assertThat(pojo.fields.size, `is`(3))
- assertThat(pojo.fields[1].name, `is`("x"))
- assertThat(pojo.fields[2].name, `is`("y"))
- assertThat(pojo.fields[0].parent, nullValue())
- assertThat(pojo.fields[1].parent, notNullValue())
- assertThat(pojo.fields[2].parent, notNullValue())
- val parent = pojo.fields[2].parent!!
- assertThat(parent.prefix, `is`(""))
- assertThat(parent.field.name, `is`("myPoint"))
- assertThat(parent.pojo.typeName,
- `is`(ClassName.get("foo.bar.MyPojo", "Point") as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun embeddedWithPrefix() {
- singleRun(
- """
- int id;
- @Embedded(prefix = "foo")
- Point myPoint;
- static class Point {
- int x;
- @ColumnInfo(name = "y2")
- int y;
- }
- """
- ) { pojo ->
- assertThat(pojo.fields.size, `is`(3))
- assertThat(pojo.fields[1].name, `is`("x"))
- assertThat(pojo.fields[2].name, `is`("y"))
- assertThat(pojo.fields[1].columnName, `is`("foox"))
- assertThat(pojo.fields[2].columnName, `is`("fooy2"))
- val parent = pojo.fields[2].parent!!
- assertThat(parent.prefix, `is`("foo"))
- }.compilesWithoutError()
- }
-
- @Test
- fun nestedEmbedded() {
- singleRun(
- """
- int id;
- @Embedded(prefix = "foo")
- Point myPoint;
- static class Point {
- int x;
- @ColumnInfo(name = "y2")
- int y;
- @Embedded(prefix = "bar")
- Coordinate coordinate;
- }
- static class Coordinate {
- double lat;
- double lng;
- @Ignore
- String ignored;
- }
- """
- ) { pojo ->
- assertThat(pojo.fields.size, `is`(5))
- assertThat(pojo.fields.map { it.columnName }, `is`(
- listOf("id", "foox", "fooy2", "foobarlat", "foobarlng")))
- }.compilesWithoutError()
- }
-
- @Test
- fun duplicateColumnNames() {
- singleRun(
- """
- int id;
- @ColumnInfo(name = "id")
- int another;
- """
- ) { _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.pojoDuplicateFieldNames("id", listOf("id", "another"))
- ).and().withErrorContaining(
- POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
- ).and().withErrorCount(3)
- }
-
- @Test
- fun duplicateColumnNamesFromEmbedded() {
- singleRun(
- """
- int id;
- @Embedded
- Foo foo;
- static class Foo {
- @ColumnInfo(name = "id")
- int x;
- }
- """
- ) { _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.pojoDuplicateFieldNames("id", listOf("id", "foo > x"))
- ).and().withErrorContaining(
- POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
- ).and().withErrorCount(3)
- }
-
- @Test
- fun dropSubPrimaryKeyNoWarningForPojo() {
- singleRun(
- """
- @PrimaryKey
- int id;
- @Embedded
- Point myPoint;
- static class Point {
- @PrimaryKey
- int x;
- int y;
- }
- """
- ) { _ ->
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun relation_notCollection() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- public User user;
- """, COMMON.USER
- ) { _ ->
- }.failsToCompile().withErrorContaining(RELATION_NOT_COLLECTION)
- }
-
- @Test
- fun relation_columnInfo() {
- singleRun(
- """
- int id;
- @ColumnInfo
- @Relation(parentColumn = "id", entityColumn = "uid")
- public List<User> user;
- """, COMMON.USER
- ) { _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION)
- }
-
- @Test
- fun relation_notEntity() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- public List<NotAnEntity> user;
- """, COMMON.NOT_AN_ENTITY
- ) { _ ->
- }.failsToCompile().withErrorContaining(ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY)
- }
-
- @Test
- fun relation_missingParent() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "idk", entityColumn = "uid")
- public List<User> user;
- """, COMMON.USER
- ) { _ ->
- }.failsToCompile().withErrorContaining(
- relationCannotFindParentEntityField("foo.bar.MyPojo", "idk", listOf("id"))
- )
- }
-
- @Test
- fun relation_missingEntityField() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "idk")
- public List<User> user;
- """, COMMON.USER
- ) { _ ->
- }.failsToCompile().withErrorContaining(
- relationCannotFindEntityField("foo.bar.User", "idk",
- listOf("uid", "name", "lastName", "age"))
- )
- }
-
- @Test
- fun relation_missingType() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- public List<User> user;
- """
- ) { _ ->
- }.failsToCompile().withErrorContaining(CANNOT_FIND_TYPE)
- }
-
- @Test
- fun relation_nestedField() {
- singleRun(
- """
- static class Nested {
- @ColumnInfo(name = "foo")
- public int id;
- }
- @Embedded
- Nested nested;
- @Relation(parentColumn = "foo", entityColumn = "uid")
- public List<User> user;
- """, COMMON.USER
- ) { pojo ->
- assertThat(pojo.relations.first().parentField.columnName, `is`("foo"))
- }.compilesWithoutError()
- }
-
- @Test
- fun relation_nestedRelation() {
- singleRun(
- """
- static class UserWithNested {
- @Embedded
- public User user;
- @Relation(parentColumn = "uid", entityColumn = "uid")
- public List<User> selfs;
- }
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid", entity = User.class)
- public List<UserWithNested> user;
- """, COMMON.USER
- ) { pojo, _ ->
- assertThat(pojo.relations.first().parentField.name, `is`("id"))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun relation_affinityMismatch() {
- singleRun(
- """
- String id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- public List<User> user;
- """, COMMON.USER
- ) { pojo, invocation ->
- // trigger assignment evaluation
- RelationCollector.createCollectors(invocation.context, pojo.relations)
- assertThat(pojo.relations.size, `is`(1))
- assertThat(pojo.relations.first().entityField.name, `is`("uid"))
- assertThat(pojo.relations.first().parentField.name, `is`("id"))
- }.compilesWithoutError().withWarningContaining(
- ProcessorErrors.relationAffinityMismatch(
- parentAffinity = SQLTypeAffinity.TEXT,
- childAffinity = SQLTypeAffinity.INTEGER,
- parentColumn = "id",
- childColumn = "uid")
- )
- }
-
- @Test
- fun relation_simple() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- public List<User> user;
- """, COMMON.USER
- ) { pojo ->
- assertThat(pojo.relations.size, `is`(1))
- assertThat(pojo.relations.first().entityField.name, `is`("uid"))
- assertThat(pojo.relations.first().parentField.name, `is`("id"))
- }.compilesWithoutError().withWarningCount(0)
- }
-
- @Test
- fun relation_badProjection() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid", projection={"i_dont_exist"})
- public List<User> user;
- """, COMMON.USER
- ) { _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.relationBadProject("foo.bar.User", listOf("i_dont_exist"),
- listOf("uid", "name", "lastName", "ageColumn"))
- )
- }
-
- @Test
- fun relation_badReturnTypeInGetter() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid")
- private List<User> user;
- public void setUser(List<User> user){ this.user = user;}
- public User getUser(){return null;}
- """, COMMON.USER
- ) { _ ->
- }.failsToCompile().withErrorContaining(CANNOT_FIND_GETTER_FOR_FIELD)
- }
-
- @Test
- fun relation_primitiveList() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid", projection={"uid"},
- entity = User.class)
- public List<Integer> userIds;
- """, COMMON.USER
- ) { pojo ->
- assertThat(pojo.relations.size, `is`(1))
- val rel = pojo.relations.first()
- assertThat(rel.projection, `is`(listOf("uid")))
- assertThat(rel.entity.typeName, `is`(COMMON.USER_TYPE_NAME as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun relation_stringList() {
- singleRun(
- """
- int id;
- @Relation(parentColumn = "id", entityColumn = "uid", projection={"name"},
- entity = User.class)
- public List<String> userNames;
- """, COMMON.USER
- ) { pojo ->
- assertThat(pojo.relations.size, `is`(1))
- val rel = pojo.relations.first()
- assertThat(rel.projection, `is`(listOf("name")))
- assertThat(rel.entity.typeName, `is`(COMMON.USER_TYPE_NAME as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun cache() {
- val pojo = """
- $HEADER
- int id;
- $FOOTER
- """.toJFO(MY_POJO.toString())
- simpleRun(pojo) { invocation ->
- val element = invocation.typeElement(MY_POJO.toString())
- val pojo1 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.BIND_TO_STMT, null).process()
- assertThat(pojo1, notNullValue())
- val pojo2 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.BIND_TO_STMT, null).process()
- assertThat(pojo2, sameInstance(pojo1))
-
- val pojo3 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.READ_FROM_CURSOR, null).process()
- assertThat(pojo3, notNullValue())
- assertThat(pojo3, not(sameInstance(pojo1)))
-
- val pojo4 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.TWO_WAY, null).process()
- assertThat(pojo4, notNullValue())
- assertThat(pojo4, not(sameInstance(pojo1)))
- assertThat(pojo4, not(sameInstance(pojo3)))
-
- val pojo5 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.TWO_WAY, null).process()
- assertThat(pojo5, sameInstance(pojo4))
-
- val type = invocation.context.COMMON_TYPES.STRING
- val mockElement = mock(Element::class.java)
- doReturn(type).`when`(mockElement).asType()
- val fakeField = Field(
- element = mockElement,
- name = "foo",
- type = type,
- affinity = SQLTypeAffinity.TEXT,
- columnName = "foo",
- parent = null,
- indexed = false
- )
- val fakeEmbedded = EmbeddedField(fakeField, "", null)
-
- val pojo6 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.TWO_WAY, fakeEmbedded).process()
- assertThat(pojo6, notNullValue())
- assertThat(pojo6, not(sameInstance(pojo1)))
- assertThat(pojo6, not(sameInstance(pojo3)))
- assertThat(pojo6, not(sameInstance(pojo4)))
-
- val pojo7 = PojoProcessor(invocation.context, element,
- FieldProcessor.BindingScope.TWO_WAY, fakeEmbedded).process()
- assertThat(pojo7, sameInstance(pojo6))
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_empty() {
- val pojoCode = """
- public String mName;
- """
- singleRun(pojoCode) { pojo ->
- assertThat(pojo.constructor, notNullValue())
- assertThat(pojo.constructor?.params, `is`(emptyList<Constructor.Param>()))
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_ambiguous_twoFieldsExactMatch() {
- val pojoCode = """
- public String mName;
- public String _name;
- public MyPojo(String mName) {
- }
- """
- singleRun(pojoCode) { pojo ->
- val param = pojo.constructor?.params?.first()
- assertThat(param, instanceOf(Constructor.FieldParam::class.java))
- assertThat((param as Constructor.FieldParam).field.name, `is`("mName"))
- assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
- `is`(CallType.CONSTRUCTOR))
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_ambiguous_oneTypeMatches() {
- val pojoCode = """
- public String mName;
- public int _name;
- public MyPojo(String name) {
- }
- """
- singleRun(pojoCode) { pojo ->
- val param = pojo.constructor?.params?.first()
- assertThat(param, instanceOf(Constructor.FieldParam::class.java))
- assertThat((param as Constructor.FieldParam).field.name, `is`("mName"))
- assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
- `is`(CallType.CONSTRUCTOR))
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_ambiguous_twoFields() {
- val pojo = """
- String mName;
- String _name;
- public MyPojo(String name) {
- }
- """
- singleRun(pojo) { _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.ambigiousConstructor(MY_POJO.toString(),
- "name", listOf("mName", "_name"))
- )
- }
-
- @Test
- fun constructor_noMatchBadType() {
- singleRun("""
- int foo;
- public MyPojo(String foo) {
- }
- """) { _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
- }
-
- @Test
- fun constructor_noMatch() {
- singleRun("""
- String mName;
- String _name;
- public MyPojo(String foo) {
- }
- """) { _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
- }
-
- @Test
- fun constructor_noMatchMultiArg() {
- singleRun("""
- String mName;
- int bar;
- public MyPojo(String foo, String name) {
- }
- """) { _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
- }
-
- @Test
- fun constructor_multipleMatching() {
- singleRun("""
- String mName;
- String mLastName;
- public MyPojo(String name) {
- }
- public MyPojo(String name, String lastName) {
- }
- """) { _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS)
- }
-
- @Test
- fun constructor_multipleMatchingWithIgnored() {
- singleRun("""
- String mName;
- String mLastName;
- @Ignore
- public MyPojo(String name) {
- }
- public MyPojo(String name, String lastName) {
- }
- """) { pojo ->
- assertThat(pojo.constructor, notNullValue())
- assertThat(pojo.constructor?.params?.size, `is`(2))
- assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
- `is`(CallType.CONSTRUCTOR))
- assertThat(pojo.fields.find { it.name == "mLastName" }?.setter?.callType,
- `is`(CallType.CONSTRUCTOR))
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_dontTryForBindToScope() {
- singleRun("""
- String mName;
- String mLastName;
- """) { _, invocation ->
- val process2 = PojoProcessor(baseContext = invocation.context,
- element = invocation.typeElement(MY_POJO.toString()),
- bindingScope = FieldProcessor.BindingScope.BIND_TO_STMT,
- parent = null).process()
- assertThat(process2.constructor, nullValue())
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_bindForTwoWay() {
- singleRun("""
- String mName;
- String mLastName;
- """) { _, invocation ->
- val process2 = PojoProcessor(baseContext = invocation.context,
- element = invocation.typeElement(MY_POJO.toString()),
- bindingScope = FieldProcessor.BindingScope.TWO_WAY,
- parent = null).process()
- assertThat(process2.constructor, notNullValue())
- }.compilesWithoutError()
- }
-
- @Test
- fun constructor_multipleMatching_withNoArg() {
- singleRun("""
- String mName;
- String mLastName;
- public MyPojo() {
- }
- public MyPojo(String name, String lastName) {
- }
- """) { pojo ->
- assertThat(pojo.constructor?.params?.size ?: -1, `is`(0))
- }.compilesWithoutError().withWarningContaining(
- ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS_CHOOSING_NO_ARG)
- }
-
- @Test // added for b/69562125
- fun constructor_withNullabilityAnnotation() {
- singleRun("""
- String mName;
- public MyPojo(@android.support.annotation.NonNull String name) {}
- """) { pojo ->
- val constructor = pojo.constructor
- assertThat(constructor, notNullValue())
- assertThat(constructor!!.params.size, `is`(1))
- }.compilesWithoutError()
- }
-
- @Test // b/69118713 common mistake so we better provide a good explanation
- fun constructor_relationParameter() {
- singleRun("""
- @Relation(entity = foo.bar.User.class, parentColumn = "uid", entityColumn="uid",
- projection = "name")
- public List<String> items;
- public String uid;
- public MyPojo(String uid, List<String> items) {
- }
- """, COMMON.USER) { _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RELATION_CANNOT_BE_CONSTRUCTOR_PARAMETER
- )
- }
-
- @Test
- fun recursion_1Level() {
- singleRun(
- """
- @Embedded
- MyPojo myPojo;
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo"))
- }
-
- @Test
- fun recursion_2Levels_relationToEmbed() {
- singleRun(
- """
- int pojoId;
-
- @Relation(parentColumn = "pojoId", entityColumn = "entityId")
- List<MyEntity> myEntity;
-
- @Entity
- static class MyEntity {
- int entityId;
-
- @Embedded
- MyPojo myPojo;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo.MyEntity -> foo.bar.MyPojo"))
- }
-
- @Test
- fun recursion_2Levels_onlyEmbeds_pojoToEntity() {
- singleRun(
- """
- @Embedded
- A a;
-
- @Entity
- static class A {
- @Embedded
- MyPojo myPojo;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
- }
-
- @Test
- fun recursion_2Levels_onlyEmbeds_onlyPojos() {
- singleRun(
- """
- @Embedded
- A a;
- static class A {
- @Embedded
- MyPojo myPojo;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
- }
-
- @Test
- fun recursion_3Levels() {
- singleRun(
- """
- @Embedded
- A a;
- public static class A {
- @Embedded
- B b;
- }
- public static class B {
- @Embedded
- MyPojo myPojo;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo"))
- }
-
- @Test
- fun recursion_1Level_1LevelDown() {
- singleRun(
- """
- @Embedded
- A a;
- static class A {
- @Embedded
- B b;
- }
- static class B {
- @Embedded
- A a;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo.A"))
- }
-
- @Test
- fun recursion_branchAtLevel0_afterBackTrack() {
- singleRun(
- """
- @PrimaryKey
- int id;
- @Embedded
- A a;
- @Embedded
- C c;
- static class A {
- @Embedded
- B b;
- }
- static class B {
- }
- static class C {
- @Embedded
- MyPojo myPojo;
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo.C -> foo.bar.MyPojo"))
- }
-
- @Test
- fun recursion_branchAtLevel1_afterBackTrack() {
- singleRun(
- """
- @PrimaryKey
- int id;
- @Embedded
- A a;
- static class A {
- @Embedded
- B b;
- @Embedded
- MyPojo myPojo;
- }
- static class B {
- @Embedded
- C c;
- }
- static class C {
- }
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
- "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
- }
-
- fun singleRun(
- code: String, vararg jfos: JavaFileObject, handler: (Pojo) -> Unit): CompileTester {
- return singleRun(code, *jfos) { pojo, _ ->
- handler(pojo)
- }
- }
-
- fun singleRun(
- code: String, vararg jfos: JavaFileObject,
- handler: (Pojo, TestInvocation) -> Unit): CompileTester {
- val pojoJFO = """
- $HEADER
- $code
- $FOOTER
- """.toJFO(MY_POJO.toString())
- val all = (jfos.toList() + pojoJFO).toTypedArray()
- return simpleRun(*all) { invocation ->
- handler.invoke(
- PojoProcessor(baseContext = invocation.context,
- element = invocation.typeElement(MY_POJO.toString()),
- bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
- parent = null).process(),
- invocation
- )
- }
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
deleted file mode 100644
index e7a9673..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
+++ /dev/null
@@ -1,817 +0,0 @@
-/*
- * Copyright (C) 2016 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.processor
-
-import COMMON
-import android.arch.persistence.room.ColumnInfo
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.LifecyclesTypeNames
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.Table
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER
-import android.arch.persistence.room.solver.query.result.DataSourceFactoryQueryResultBinder
-import android.arch.persistence.room.solver.query.result.LiveDataQueryResultBinder
-import android.arch.persistence.room.solver.query.result.PojoRowAdapter
-import android.arch.persistence.room.solver.query.result.SingleEntityQueryResultAdapter
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.QueryMethod
-import android.arch.persistence.room.vo.Warning
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth.assertAbout
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
-import createVerifierFromEntities
-import mockElementAndType
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.hasItem
-import org.hamcrest.CoreMatchers.instanceOf
-import org.hamcrest.CoreMatchers.not
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.Mockito
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.TypeKind.INT
-import javax.lang.model.type.TypeMirror
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-@RunWith(Parameterized::class)
-class QueryMethodProcessorTest(val enableVerification: Boolean) {
- companion object {
- const val DAO_PREFIX = """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
- @Dao
- abstract class MyClass {
- """
- const val DAO_SUFFIX = "}"
- val POJO: ClassName = ClassName.get("foo.bar", "MyClass.Pojo")
- @Parameterized.Parameters(name = "enableDbVerification={0}")
- @JvmStatic
- fun getParams() = arrayOf(true, false)
-
- fun createField(name: String, columnName: String? = null): Field {
- val (element, type) = mockElementAndType()
- return Field(
- element = element,
- name = name,
- type = type,
- columnName = columnName ?: name,
- affinity = null
- )
- }
- }
-
- @Test
- fun testReadNoParams() {
- singleQueryMethod(
- """
- @Query("SELECT * from User")
- abstract public int[] foo();
- """) { parsedQuery, _ ->
- assertThat(parsedQuery.name, `is`("foo"))
- assertThat(parsedQuery.parameters.size, `is`(0))
- assertThat(parsedQuery.returnType.typeName(),
- `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun testSingleParam() {
- singleQueryMethod(
- """
- @Query("SELECT * from User where uid = :x")
- abstract public long foo(int x);
- """) { parsedQuery, invocation ->
- assertThat(parsedQuery.name, `is`("foo"))
- assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG))
- assertThat(parsedQuery.parameters.size, `is`(1))
- val param = parsedQuery.parameters.first()
- assertThat(param.name, `is`("x"))
- assertThat(param.sqlName, `is`("x"))
- assertThat(param.type,
- `is`(invocation.processingEnv.typeUtils.getPrimitiveType(INT) as TypeMirror))
- }.compilesWithoutError()
- }
-
- @Test
- fun testVarArgs() {
- singleQueryMethod(
- """
- @Query("SELECT * from User where uid in (:ids)")
- abstract public long foo(int... ids);
- """) { parsedQuery, invocation ->
- assertThat(parsedQuery.name, `is`("foo"))
- assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG))
- assertThat(parsedQuery.parameters.size, `is`(1))
- val param = parsedQuery.parameters.first()
- assertThat(param.name, `is`("ids"))
- assertThat(param.sqlName, `is`("ids"))
- val types = invocation.processingEnv.typeUtils
- assertThat(param.type,
- `is`(types.getArrayType(types.getPrimitiveType(INT)) as TypeMirror))
- }.compilesWithoutError()
- }
-
- @Test
- fun testParamBindingMatchingNoName() {
- singleQueryMethod(
- """
- @Query("SELECT uid from User where uid = :id")
- abstract public long getIdById(int id);
- """) { parsedQuery, _ ->
- val section = parsedQuery.query.bindSections.first()
- val param = parsedQuery.parameters.firstOrNull()
- assertThat(section, notNullValue())
- assertThat(param, notNullValue())
- assertThat(parsedQuery.sectionToParamMapping, `is`(listOf(Pair(section, param))))
- }.compilesWithoutError()
- }
-
- @Test
- fun testParamBindingMatchingSimpleBind() {
- singleQueryMethod(
- """
- @Query("SELECT uid from User where uid = :id")
- abstract public long getIdById(int id);
- """) { parsedQuery, _ ->
- val section = parsedQuery.query.bindSections.first()
- val param = parsedQuery.parameters.firstOrNull()
- assertThat(section, notNullValue())
- assertThat(param, notNullValue())
- assertThat(parsedQuery.sectionToParamMapping,
- `is`(listOf(Pair(section, param))))
- }.compilesWithoutError()
- }
-
- @Test
- fun testParamBindingTwoBindVarsIntoTheSameParameter() {
- singleQueryMethod(
- """
- @Query("SELECT uid from User where uid = :id OR uid = :id")
- abstract public long getIdById(int id);
- """) { parsedQuery, _ ->
- val section = parsedQuery.query.bindSections[0]
- val section2 = parsedQuery.query.bindSections[1]
- val param = parsedQuery.parameters.firstOrNull()
- assertThat(section, notNullValue())
- assertThat(section2, notNullValue())
- assertThat(param, notNullValue())
- assertThat(parsedQuery.sectionToParamMapping,
- `is`(listOf(Pair(section, param), Pair(section2, param))))
- }.compilesWithoutError()
- }
-
- @Test
- fun testMissingParameterForBinding() {
- singleQueryMethod(
- """
- @Query("SELECT uid from User where uid = :id OR uid = :uid")
- abstract public long getIdById(int id);
- """) { parsedQuery, _ ->
- val section = parsedQuery.query.bindSections[0]
- val section2 = parsedQuery.query.bindSections[1]
- val param = parsedQuery.parameters.firstOrNull()
- assertThat(section, notNullValue())
- assertThat(section2, notNullValue())
- assertThat(param, notNullValue())
- assertThat(parsedQuery.sectionToParamMapping,
- `is`(listOf(Pair(section, param), Pair(section2, null))))
- }
- .failsToCompile()
- .withErrorContaining(
- ProcessorErrors.missingParameterForBindVariable(listOf(":uid")))
- }
-
- @Test
- fun test2MissingParameterForBinding() {
- singleQueryMethod(
- """
- @Query("SELECT uid from User where name = :bar AND uid = :id OR uid = :uid")
- abstract public long getIdById(int id);
- """) { parsedQuery, _ ->
- val bar = parsedQuery.query.bindSections[0]
- val id = parsedQuery.query.bindSections[1]
- val uid = parsedQuery.query.bindSections[2]
- val param = parsedQuery.parameters.firstOrNull()
- assertThat(bar, notNullValue())
- assertThat(id, notNullValue())
- assertThat(uid, notNullValue())
- assertThat(param, notNullValue())
- assertThat(parsedQuery.sectionToParamMapping,
- `is`(listOf(Pair(bar, null), Pair(id, param), Pair(uid, null))))
- }
- .failsToCompile()
- .withErrorContaining(
- ProcessorErrors.missingParameterForBindVariable(listOf(":bar", ":uid")))
- }
-
- @Test
- fun testUnusedParameters() {
- singleQueryMethod(
- """
- @Query("SELECT uid from User where name = :bar")
- abstract public long getIdById(int bar, int whyNotUseMe);
- """) { parsedQuery, _ ->
- val bar = parsedQuery.query.bindSections[0]
- val barParam = parsedQuery.parameters.firstOrNull()
- assertThat(bar, notNullValue())
- assertThat(barParam, notNullValue())
- assertThat(parsedQuery.sectionToParamMapping,
- `is`(listOf(Pair(bar, barParam))))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.unusedQueryMethodParameter(listOf("whyNotUseMe")))
- }
-
- @Test
- fun testNameWithUnderscore() {
- singleQueryMethod(
- """
- @Query("select * from User where uid = :_blah")
- abstract public long getSth(int _blah);
- """
- ) { _, _ -> }
- .failsToCompile()
- .withErrorContaining(ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
- }
-
- @Test
- fun testGenericReturnType() {
- singleQueryMethod(
- """
- @Query("select * from User")
- abstract public <T> ${CommonTypeNames.LIST}<T> foo(int x);
- """) { parsedQuery, _ ->
- val expected: TypeName = ParameterizedTypeName.get(ClassName.get(List::class.java),
- TypeVariableName.get("T"))
- assertThat(parsedQuery.returnType.typeName(), `is`(expected))
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
- }
-
- @Test
- fun testBadQuery() {
- singleQueryMethod(
- """
- @Query("select * from :1 :2")
- abstract public long foo(int x);
- """) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining("UNEXPECTED_CHAR=:")
- }
-
- @Test
- fun testLiveDataWithWithClause() {
- singleQueryMethod(
- """
- @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
- + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User")
- abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
- getFactorialLiveData();
- """) { parsedQuery, _ ->
- assertThat(parsedQuery.query.tables, hasItem(Table("User", "User")))
- assertThat(parsedQuery.query.tables,
- not(hasItem(Table("tempTable", "tempTable"))))
- assertThat(parsedQuery.query.tables.size, `is`(1))
- }.compilesWithoutError()
- }
-
- @Test
- fun testLiveDataWithNothingToObserve() {
- singleQueryMethod(
- """
- @Query("SELECT 1")
- abstract public ${LifecyclesTypeNames.LIVE_DATA}<Integer> getOne();
- """) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
-
- @Test
- fun testLiveDataWithWithClauseAndNothingToObserve() {
- singleQueryMethod(
- """
- @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
- + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable")
- abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
- getFactorialLiveData();
- """) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
-
- @Test
- fun testBoundGeneric() {
- singleQueryMethod(
- """
- static abstract class BaseModel<T> {
- @Query("select COUNT(*) from User")
- abstract public T getT();
- }
- @Dao
- static abstract class ExtendingModel extends BaseModel<Integer> {
- }
- """) { parsedQuery, _ ->
- assertThat(parsedQuery.returnType.typeName(),
- `is`(ClassName.get(Integer::class.java) as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun testBoundGenericParameter() {
- singleQueryMethod(
- """
- static abstract class BaseModel<T> {
- @Query("select COUNT(*) from User where :t")
- abstract public int getT(T t);
- }
- @Dao
- static abstract class ExtendingModel extends BaseModel<Integer> {
- }
- """) { parsedQuery, invocation ->
- assertThat(parsedQuery.parameters.first().type,
- `is`(invocation.processingEnv.elementUtils
- .getTypeElement("java.lang.Integer").asType()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testReadDeleteWithBadReturnType() {
- singleQueryMethod(
- """
- @Query("DELETE from User where uid = :id")
- abstract public float foo(int id);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
- )
- }
-
- @Test
- fun testSimpleDelete() {
- singleQueryMethod(
- """
- @Query("DELETE from User where uid = :id")
- abstract public int foo(int id);
- """) { parsedQuery, _ ->
- assertThat(parsedQuery.name, `is`("foo"))
- assertThat(parsedQuery.parameters.size, `is`(1))
- assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.INT))
- }.compilesWithoutError()
- }
-
- @Test
- fun testVoidDeleteQuery() {
- singleQueryMethod(
- """
- @Query("DELETE from User where uid = :id")
- abstract public void foo(int id);
- """) { parsedQuery, _ ->
- assertThat(parsedQuery.name, `is`("foo"))
- assertThat(parsedQuery.parameters.size, `is`(1))
- assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID))
- }.compilesWithoutError()
- }
-
- @Test
- fun testVoidUpdateQuery() {
- singleQueryMethod(
- """
- @Query("update user set name = :name")
- abstract public void updateAllNames(String name);
- """) { parsedQuery, invocation ->
- assertThat(parsedQuery.name, `is`("updateAllNames"))
- assertThat(parsedQuery.parameters.size, `is`(1))
- assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID))
- assertThat(parsedQuery.parameters.first().type.typeName(),
- `is`(invocation.context.COMMON_TYPES.STRING.typeName()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testLiveDataQuery() {
- singleQueryMethod(
- """
- @Query("select name from user where uid = :id")
- abstract ${LifecyclesTypeNames.LIVE_DATA}<String> nameLiveData(String id);
- """
- ) { parsedQuery, _ ->
- assertThat(parsedQuery.returnType.typeName(),
- `is`(ParameterizedTypeName.get(LifecyclesTypeNames.LIVE_DATA,
- String::class.typeName()) as TypeName))
- assertThat(parsedQuery.queryResultBinder,
- instanceOf(LiveDataQueryResultBinder::class.java))
- }.compilesWithoutError()
- }
-
- @Test
- fun testNonSelectLiveData() {
- singleQueryMethod(
- """
- @Query("delete from user where uid = :id")
- abstract ${LifecyclesTypeNames.LIVE_DATA}<Integer> deleteLiveData(String id);
- """
- ) { _, _ ->
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT)
- }
-
- @Test
- fun testDataSourceFactoryQuery() {
- singleQueryMethod(
- """
- @Query("select name from user")
- abstract ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, String>
- nameDataSourceFactory();
- """
- ) { parsedQuery, _ ->
- assertThat(parsedQuery.returnType.typeName(),
- `is`(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY,
- Integer::class.typeName(), String::class.typeName()) as TypeName))
- assertThat(parsedQuery.queryResultBinder,
- instanceOf(DataSourceFactoryQueryResultBinder::class.java))
- val tableNames =
- (parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder)
- .positionalDataSourceQueryResultBinder.tableNames
- assertEquals(setOf("user"), tableNames)
- }.compilesWithoutError()
- }
-
- @Test
- fun testMultiTableDataSourceFactoryQuery() {
- singleQueryMethod(
- """
- @Query("select name from User u LEFT OUTER JOIN Book b ON u.uid == b.uid")
- abstract ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, String>
- nameDataSourceFactory();
- """
- ) { parsedQuery, _ ->
- assertThat(parsedQuery.returnType.typeName(),
- `is`(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY,
- Integer::class.typeName(), String::class.typeName()) as TypeName))
- assertThat(parsedQuery.queryResultBinder,
- instanceOf(DataSourceFactoryQueryResultBinder::class.java))
- val tableNames =
- (parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder)
- .positionalDataSourceQueryResultBinder.tableNames
- assertEquals(setOf("User", "Book"), tableNames)
- }.compilesWithoutError()
- }
-
- @Test
- fun query_detectTransaction_delete() {
- singleQueryMethod(
- """
- @Query("delete from user where uid = :id")
- abstract int deleteUser(String id);
- """
- ) { method, _ ->
- assertThat(method.inTransaction, `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun query_detectTransaction_update() {
- singleQueryMethod(
- """
- @Query("UPDATE user set uid = :id + 1 where uid = :id")
- abstract int incrementId(String id);
- """
- ) { method, _ ->
- assertThat(method.inTransaction, `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun query_detectTransaction_select() {
- singleQueryMethod(
- """
- @Query("select * from user")
- abstract int loadUsers();
- """
- ) { method, _ ->
- assertThat(method.inTransaction, `is`(false))
- }.compilesWithoutError()
- }
-
- @Test
- fun query_detectTransaction_selectInTransaction() {
- singleQueryMethod(
- """
- @Transaction
- @Query("select * from user")
- abstract int loadUsers();
- """
- ) { method, _ ->
- assertThat(method.inTransaction, `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun skipVerification() {
- singleQueryMethod(
- """
- @SkipQueryVerification
- @Query("SELECT foo from User")
- abstract public int[] foo();
- """) { parsedQuery, _ ->
- assertThat(parsedQuery.name, `is`("foo"))
- assertThat(parsedQuery.parameters.size, `is`(0))
- assertThat(parsedQuery.returnType.typeName(),
- `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun suppressWarnings() {
- singleQueryMethod("""
- @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
- @Query("SELECT uid from User")
- abstract public int[] foo();
- """) { method, invocation ->
- assertThat(QueryMethodProcessor(
- baseContext = invocation.context,
- containing = Mockito.mock(DeclaredType::class.java),
- executableElement = method.element,
- dbVerifier = null).context.logger.suppressedWarnings
- , `is`(setOf(Warning.CURSOR_MISMATCH)))
- }.compilesWithoutError()
- }
-
- @Test
- fun pojo_renamedColumn() {
- pojoTest("""
- String name;
- String lName;
- """, listOf("name", "lastName as lName")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
- assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
- }?.compilesWithoutError()?.withWarningCount(0)
- }
-
- @Test
- fun pojo_exactMatch() {
- pojoTest("""
- String name;
- String lastName;
- """, listOf("name", "lastName")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
- assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
- }?.compilesWithoutError()?.withWarningCount(0)
- }
-
- @Test
- fun pojo_exactMatchWithStar() {
- pojoTest("""
- String name;
- String lastName;
- int uid;
- @ColumnInfo(name = "ageColumn")
- int age;
- """, listOf("*")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
- assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
- }?.compilesWithoutError()?.withWarningCount(0)
- }
-
- @Test
- fun pojo_nonJavaName() {
- pojoTest("""
- @ColumnInfo(name = "MAX(ageColumn)")
- int maxAge;
- String name;
- """, listOf("MAX(ageColumn)", "name")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
- assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
- }?.compilesWithoutError()?.withWarningCount(0)
- }
-
- @Test
- fun pojo_noMatchingFields() {
- pojoTest("""
- String nameX;
- String lastNameX;
- """, listOf("name", "lastName")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("name", "lastName")))
- assertThat(adapter?.mapping?.unusedFields, `is`(adapter?.pojo?.fields))
- }?.failsToCompile()
- ?.withErrorContaining(CANNOT_FIND_QUERY_RESULT_ADAPTER)
- ?.and()
- ?.withWarningContaining(
- ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = POJO,
- unusedColumns = listOf("name", "lastName"),
- unusedFields = listOf(createField("nameX"),
- createField("lastNameX")),
- allColumns = listOf("name", "lastName"),
- allFields = listOf(
- createField("nameX"),
- createField("lastNameX")
- )
- )
- )
- }
-
- @Test
- fun pojo_badQuery() {
- // do not report mismatch if query is broken
- pojoTest("""
- @ColumnInfo(name = "MAX(ageColumn)")
- int maxAge;
- String name;
- """, listOf("MAX(age)", "name")) { _, _, _ ->
- }?.failsToCompile()
- ?.withErrorContaining("no such column: age")
- ?.and()
- ?.withErrorCount(1)
- ?.withWarningCount(0)
- }
-
- @Test
- fun pojo_tooManyColumns() {
- pojoTest("""
- String name;
- String lastName;
- """, listOf("uid", "name", "lastName")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
- assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
- }?.compilesWithoutError()?.withWarningContaining(
- ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = POJO,
- unusedColumns = listOf("uid"),
- unusedFields = emptyList(),
- allColumns = listOf("uid", "name", "lastName"),
- allFields = listOf(createField("name"), createField("lastName"))
- ))
- }
-
- @Test
- fun pojo_tooManyFields() {
- pojoTest("""
- String name;
- String lastName;
- """, listOf("lastName")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
- assertThat(adapter?.mapping?.unusedFields, `is`(
- adapter?.pojo?.fields?.filter { it.name == "name" }
- ))
- }?.compilesWithoutError()?.withWarningContaining(
- ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = POJO,
- unusedColumns = emptyList(),
- unusedFields = listOf(createField("name")),
- allColumns = listOf("lastName"),
- allFields = listOf(createField("name"), createField("lastName"))
- ))
- }
-
- @Test
- fun pojo_missingNonNull() {
- pojoTest("""
- @NonNull
- String name;
- String lastName;
- """, listOf("lastName")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
- assertThat(adapter?.mapping?.unusedFields, `is`(
- adapter?.pojo?.fields?.filter { it.name == "name" }
- ))
- }?.failsToCompile()?.withWarningContaining(
- ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = POJO,
- unusedColumns = emptyList(),
- unusedFields = listOf(createField("name")),
- allColumns = listOf("lastName"),
- allFields = listOf(createField("name"), createField("lastName"))
- ))?.and()?.withErrorContaining(
- ProcessorErrors.pojoMissingNonNull(pojoTypeName = POJO,
- missingPojoFields = listOf("name"),
- allQueryColumns = listOf("lastName")))
- }
-
- @Test
- fun pojo_tooManyFieldsAndColumns() {
- pojoTest("""
- String name;
- String lastName;
- """, listOf("uid", "name")) { adapter, _, _ ->
- assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
- assertThat(adapter?.mapping?.unusedFields, `is`(
- adapter?.pojo?.fields?.filter { it.name == "lastName" }
- ))
- }?.compilesWithoutError()?.withWarningContaining(
- ProcessorErrors.cursorPojoMismatch(
- pojoTypeName = POJO,
- unusedColumns = listOf("uid"),
- unusedFields = listOf(createField("lastName")),
- allColumns = listOf("uid", "name"),
- allFields = listOf(createField("name"), createField("lastName"))
- ))
- }
-
- fun pojoTest(pojoFields: String, queryColumns: List<String>,
- handler: (PojoRowAdapter?, QueryMethod, TestInvocation) -> Unit): CompileTester? {
- val assertion = singleQueryMethod(
- """
- static class Pojo {
- $pojoFields
- }
- @Query("SELECT ${queryColumns.joinToString(", ")} from User LIMIT 1")
- abstract MyClass.Pojo getNameAndLastNames();
- """
- ) { parsedQuery, invocation ->
- val adapter = parsedQuery.queryResultBinder.adapter
- if (enableVerification) {
- if (adapter is SingleEntityQueryResultAdapter) {
- handler(adapter.rowAdapter as? PojoRowAdapter, parsedQuery, invocation)
- } else {
- handler(null, parsedQuery, invocation)
- }
- } else {
- assertThat(adapter, nullValue())
- }
- }
- if (enableVerification) {
- return assertion
- } else {
- assertion.failsToCompile().withErrorContaining(CANNOT_FIND_QUERY_RESULT_ADAPTER)
- return null
- }
- }
-
- fun singleQueryMethod(vararg input: String,
- handler: (QueryMethod, TestInvocation) -> Unit):
- CompileTester {
- return assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
- DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
- ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER, COMMON.BOOK))
- .processedWith(TestProcessor.builder()
- .forAnnotations(Query::class, Dao::class, ColumnInfo::class,
- Entity::class, PrimaryKey::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.java)
- .map {
- Pair(it,
- invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .filter {
- it.hasAnnotation(Query::class)
- }
- )
- }.filter { it.second.isNotEmpty() }.first()
- val verifier = if (enableVerification) {
- createVerifierFromEntities(invocation)
- } else {
- null
- }
- val parser = QueryMethodProcessor(
- baseContext = invocation.context,
- containing = MoreTypes.asDeclared(owner.asType()),
- executableElement = MoreElements.asExecutable(methods.first()),
- dbVerifier = verifier)
- val parsedQuery = parser.process()
- handler(parsedQuery, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
deleted file mode 100644
index 0a9be4a..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright 2018 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.processor
-
-import COMMON
-import android.arch.persistence.room.ColumnInfo
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.RawQuery
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.ext.SupportDbTypeNames
-import android.arch.persistence.room.ext.hasAnnotation
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.RawQueryMethod
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeName
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-
-class RawQueryMethodProcessorTest {
- @Test
- fun supportRawQuery() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public int[] foo(SupportSQLiteQuery query);
- """) { query, _ ->
- assertThat(query.name, `is`("foo"))
- assertThat(query.runtimeQueryParam, `is`(
- RawQueryMethod.RuntimeQueryParameter(
- paramName = "query",
- type = SupportDbTypeNames.QUERY
- )
- ))
- assertThat(query.returnType.typeName(),
- `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun stringRawQuery() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public int[] foo(String query);
- """) { query, _ ->
- assertThat(query.name, `is`("foo"))
- assertThat(query.runtimeQueryParam, `is`(
- RawQueryMethod.RuntimeQueryParameter(
- paramName = "query",
- type = CommonTypeNames.STRING
- )
- ))
- assertThat(query.returnType.typeName(),
- `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
- }.compilesWithoutError()
- }
-
- @Test
- fun withObservedEntities() {
- singleQueryMethod(
- """
- @RawQuery(observedEntities = User.class)
- abstract public LiveData<User> foo(SupportSQLiteQuery query);
- """) { query, _ ->
- assertThat(query.name, `is`("foo"))
- assertThat(query.runtimeQueryParam, `is`(
- RawQueryMethod.RuntimeQueryParameter(
- paramName = "query",
- type = SupportDbTypeNames.QUERY
- )
- ))
- assertThat(query.observedTableNames.size, `is`(1))
- assertThat(query.observedTableNames, `is`(setOf("User")))
- }.compilesWithoutError()
- }
-
- @Test
- fun observableWithoutEntities() {
- singleQueryMethod(
- """
- @RawQuery(observedEntities = {})
- abstract public LiveData<User> foo(SupportSQLiteQuery query);
- """) { query, _ ->
- assertThat(query.name, `is`("foo"))
- assertThat(query.runtimeQueryParam, `is`(
- RawQueryMethod.RuntimeQueryParameter(
- paramName = "query",
- type = SupportDbTypeNames.QUERY
- )
- ))
- assertThat(query.observedTableNames, `is`(emptySet()))
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
-
- @Test
- fun observableWithoutEntities_dataSourceFactory() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, User> getOne();
- """) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
-
- @Test
- fun observableWithoutEntities_positionalDataSource() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
- """) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
- }
-
- @Test
- fun positionalDataSource() {
- singleQueryMethod(
- """
- @RawQuery(observedEntities = {User.class})
- abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne(
- SupportSQLiteQuery query);
- """) { _, _ ->
- // do nothing
- }.compilesWithoutError()
- }
-
- @Test
- fun pojo() {
- val pojo: TypeName = ClassName.get("foo.bar.MyClass", "MyPojo")
- singleQueryMethod(
- """
- public class MyPojo {
- public String foo;
- public String bar;
- }
-
- @RawQuery
- abstract public MyPojo foo(SupportSQLiteQuery query);
- """) { query, _ ->
- assertThat(query.name, `is`("foo"))
- assertThat(query.runtimeQueryParam, `is`(
- RawQueryMethod.RuntimeQueryParameter(
- paramName = "query",
- type = SupportDbTypeNames.QUERY
- )
- ))
- assertThat(query.returnType.typeName(), `is`(pojo))
- assertThat(query.observedTableNames, `is`(emptySet()))
- }.compilesWithoutError()
- }
-
- @Test
- fun void() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public void foo(SupportSQLiteQuery query);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
- )
- }
-
- @Test
- fun noArgs() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public int[] foo();
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_PARAMS
- )
- }
-
- @Test
- fun tooManyArgs() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public int[] foo(String query, String query2);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_PARAMS
- )
- }
-
- @Test
- fun varargs() {
- singleQueryMethod(
- """
- @RawQuery
- abstract public int[] foo(String... query);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_PARAMS
- )
- }
-
- @Test
- fun observed_notAnEntity() {
- singleQueryMethod(
- """
- @RawQuery(observedEntities = {${COMMON.NOT_AN_ENTITY_TYPE_NAME}.class})
- abstract public int[] foo(String query);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.rawQueryBadEntity(COMMON.NOT_AN_ENTITY_TYPE_NAME)
- )
- }
-
- @Test
- fun observed_relationPojo() {
- singleQueryMethod(
- """
- public static class MyPojo {
- public String foo;
- @Relation(
- parentColumn = "foo",
- entityColumn = "name"
- )
- public java.util.List<User> users;
- }
- @RawQuery(observedEntities = MyPojo.class)
- abstract public int[] foo(String query);
- """) { method, _ ->
- assertThat(method.observedTableNames, `is`(setOf("User")))
- }.compilesWithoutError()
- }
-
- @Test
- fun observed_embedded() {
- singleQueryMethod(
- """
- public static class MyPojo {
- public String foo;
- @Embedded
- public User users;
- }
- @RawQuery(observedEntities = MyPojo.class)
- abstract public int[] foo(String query);
- """) { method, _ ->
- assertThat(method.observedTableNames, `is`(setOf("User")))
- }.compilesWithoutError()
- }
-
- private fun singleQueryMethod(
- vararg input: String,
- handler: (RawQueryMethod, TestInvocation) -> Unit
- ): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
- DAO_PREFIX
- + input.joinToString("\n")
- + DAO_SUFFIX
- ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
- COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
- COMMON.NOT_AN_ENTITY))
- .processedWith(TestProcessor.builder()
- .forAnnotations(Query::class, Dao::class, ColumnInfo::class,
- Entity::class, PrimaryKey::class, RawQuery::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.java)
- .map {
- Pair(it,
- invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .filter {
- it.hasAnnotation(RawQuery::class)
- }
- )
- }.first { it.second.isNotEmpty() }
- val parser = RawQueryMethodProcessor(
- baseContext = invocation.context,
- containing = MoreTypes.asDeclared(owner.asType()),
- executableElement = MoreElements.asExecutable(methods.first()))
- val parsedQuery = parser.process()
- handler(parsedQuery, invocation)
- true
- }
- .build())
- }
-
- companion object {
- private const val DAO_PREFIX = """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
- import android.arch.persistence.db.SupportSQLiteQuery;
- import android.arch.lifecycle.LiveData;
- @Dao
- abstract class MyClass {
- """
- private const val DAO_SUFFIX = "}"
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/ShortcutMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/ShortcutMethodProcessorTest.kt
deleted file mode 100644
index 7a332ea..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/ShortcutMethodProcessorTest.kt
+++ /dev/null
@@ -1,297 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.ext.CommonTypeNames
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.ShortcutMethod
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.ArrayTypeName
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import org.hamcrest.CoreMatchers
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-import kotlin.reflect.KClass
-
-/**
- * Base test class for shortcut methods.
- */
-abstract class ShortcutMethodProcessorTest<out T : ShortcutMethod>(
- val annotation: KClass<out Annotation>) {
- companion object {
- const val DAO_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import java.util.*;
- @Dao
- abstract class MyClass {
- """
- const val DAO_SUFFIX = "}"
- val USER_TYPE_NAME: TypeName = COMMON.USER_TYPE_NAME
- val BOOK_TYPE_NAME: TypeName = ClassName.get("foo.bar", "Book")
- }
-
- @Test
- fun noParams() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void foo();
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("foo"))
- assertThat(shortcut.parameters.size, `is`(0))
- assertThat(shortcut.returnCount, `is`(false))
- }.failsToCompile().withErrorContaining(noParamsError())
- }
-
- abstract fun noParamsError(): String
-
- @Test
- fun single() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public int foo(User user);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("foo"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.type.typeName(), `is`(USER_TYPE_NAME))
- assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
- assertThat(shortcut.entities.size, `is`(1))
- assertThat(shortcut.entities["user"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.returnCount, `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun notAnEntity() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void foo(NotAnEntity notValid);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("foo"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.entityType, `is`(CoreMatchers.nullValue()))
- assertThat(shortcut.entities.size, `is`(0))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
- )
- }
-
- @Test
- fun two() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void foo(User u1, User u2);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("foo"))
-
- assertThat(shortcut.parameters.size, `is`(2))
- shortcut.parameters.forEach {
- assertThat(it.type.typeName(), `is`(USER_TYPE_NAME))
- assertThat(it.entityType?.typeName(), `is`(USER_TYPE_NAME))
- }
- assertThat(shortcut.entities.size, `is`(2))
- assertThat(shortcut.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.parameters.map { it.name },
- `is`(listOf("u1", "u2")))
- assertThat(shortcut.returnCount, `is`(false))
- }.compilesWithoutError()
- }
-
- @Test
- fun list() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public int users(List<User> users);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("users"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(
- ClassName.get("java.util", "List"), USER_TYPE_NAME) as TypeName))
- assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
- assertThat(shortcut.entities.size, `is`(1))
- assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.returnCount, `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun array() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void users(User[] users);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("users"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ArrayTypeName.of(COMMON.USER_TYPE_NAME) as TypeName))
- assertThat(shortcut.entities.size, `is`(1))
- assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.returnCount, `is`(false))
- }.compilesWithoutError()
- }
-
- @Test
- fun set() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void modifyUsers(Set<User> users);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("modifyUsers"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(ClassName.get("java.util", "Set")
- , COMMON.USER_TYPE_NAME) as TypeName))
- assertThat(shortcut.entities.size, `is`(1))
- assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.returnCount, `is`(false))
- }.compilesWithoutError()
- }
-
- @Test
- fun iterable() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void modifyUsers(Iterable<User> users);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("modifyUsers"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(ClassName.get("java.lang", "Iterable")
- , COMMON.USER_TYPE_NAME) as TypeName))
- assertThat(shortcut.entities.size, `is`(1))
- assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.returnCount, `is`(false))
- }.compilesWithoutError()
- }
-
- @Test
- fun customCollection() {
- singleShortcutMethod(
- """
- static class MyList<Irrelevant, Item> extends ArrayList<Item> {}
- @${annotation.java.canonicalName}
- abstract public void modifyUsers(MyList<String, User> users);
- """) { shortcut, _ ->
- assertThat(shortcut.name, `is`("modifyUsers"))
- assertThat(shortcut.parameters.size, `is`(1))
- val param = shortcut.parameters.first()
- assertThat(param.type.typeName(), `is`(
- ParameterizedTypeName.get(ClassName.get("foo.bar", "MyClass.MyList")
- , CommonTypeNames.STRING
- , COMMON.USER_TYPE_NAME) as TypeName))
- assertThat(shortcut.entities.size, `is`(1))
- assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.returnCount, `is`(false))
- }.compilesWithoutError()
- }
-
- @Test
- fun differentTypes() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public void foo(User u1, Book b1);
- """) { shortcut, _ ->
- assertThat(shortcut.parameters.size, `is`(2))
- assertThat(shortcut.parameters[0].type.typeName().toString(),
- `is`("foo.bar.User"))
- assertThat(shortcut.parameters[1].type.typeName().toString(),
- `is`("foo.bar.Book"))
- assertThat(shortcut.parameters.map { it.name }, `is`(listOf("u1", "b1")))
- assertThat(shortcut.returnCount, `is`(false))
- assertThat(shortcut.entities.size, `is`(2))
- assertThat(shortcut.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
- assertThat(shortcut.entities["b1"]?.typeName, `is`(BOOK_TYPE_NAME))
- }.compilesWithoutError()
- }
-
- @Test
- fun invalidReturnType() {
- singleShortcutMethod(
- """
- @${annotation.java.canonicalName}
- abstract public long foo(User user);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(invalidReturnTypeError())
- }
-
- abstract fun invalidReturnTypeError(): String
-
- abstract fun process(baseContext: Context, containing: DeclaredType,
- executableElement: ExecutableElement): T
-
- fun singleShortcutMethod(vararg input: String,
- handler: (T, TestInvocation) -> Unit):
- CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
- DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
- ), COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY))
- .processedWith(TestProcessor.builder()
- .forAnnotations(annotation, Dao::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.java)
- .map {
- Pair(it,
- invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .filter {
- MoreElements.isAnnotationPresent(it,
- annotation.java)
- }
- )
- }.filter { it.second.isNotEmpty() }.first()
- val processed = process(
- baseContext = invocation.context,
- containing = MoreTypes.asDeclared(owner.asType()),
- executableElement = MoreElements.asExecutable(methods.first()))
- handler(processed, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt
deleted file mode 100644
index a07cbe6..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Transaction
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.TransactionMethod
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-@RunWith(JUnit4::class)
-class TransactionMethodProcessorTest {
-
- companion object {
- const val DAO_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import java.util.*;
- @Dao
- abstract class MyClass {
- """
- const val DAO_SUFFIX = "}"
- }
-
- @Test
- fun simple() {
- singleTransactionMethod(
- """
- @Transaction
- public String doInTransaction(int param) { return null; }
- """) { transaction, _ ->
- assertThat(transaction.name, `is`("doInTransaction"))
- }.compilesWithoutError()
- }
-
- @Test
- fun modifier_private() {
- singleTransactionMethod(
- """
- @Transaction
- private String doInTransaction(int param) { return null; }
- """) { transaction, _ ->
- assertThat(transaction.name, `is`("doInTransaction"))
- }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
- }
-
- @Test
- fun modifier_final() {
- singleTransactionMethod(
- """
- @Transaction
- public final String doInTransaction(int param) { return null; }
- """) { transaction, _ ->
- assertThat(transaction.name, `is`("doInTransaction"))
- }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
- }
-
- private fun singleTransactionMethod(vararg input: String,
- handler: (TransactionMethod, TestInvocation) -> Unit):
- CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
- TransactionMethodProcessorTest.DAO_PREFIX + input.joinToString("\n") +
- TransactionMethodProcessorTest.DAO_SUFFIX
- )))
- .processedWith(TestProcessor.builder()
- .forAnnotations(Transaction::class, Dao::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.java)
- .map {
- Pair(it,
- invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .filter {
- MoreElements.isAnnotationPresent(it,
- Transaction::class.java)
- }
- )
- }.filter { it.second.isNotEmpty() }.first()
- val processor = TransactionMethodProcessor(
- baseContext = invocation.context,
- containing = MoreTypes.asDeclared(owner.asType()),
- executableElement = MoreElements.asExecutable(methods.first()))
- val processed = processor.process()
- handler(processed, invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/UpdateMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/UpdateMethodProcessorTest.kt
deleted file mode 100644
index 84ff0f1..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/UpdateMethodProcessorTest.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.processor
-
-import android.arch.persistence.room.OnConflictStrategy
-import android.arch.persistence.room.Update
-import android.arch.persistence.room.processor.ProcessorErrors
- .UPDATE_METHODS_MUST_RETURN_VOID_OR_INT
-import android.arch.persistence.room.processor.ProcessorErrors.UPDATE_MISSING_PARAMS
-import android.arch.persistence.room.vo.UpdateMethod
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.type.DeclaredType
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-@RunWith(JUnit4::class)
-class UpdateMethodProcessorTest : ShortcutMethodProcessorTest<UpdateMethod>(Update::class) {
- override fun invalidReturnTypeError(): String = UPDATE_METHODS_MUST_RETURN_VOID_OR_INT
-
- override fun noParamsError(): String = UPDATE_MISSING_PARAMS
-
- override fun process(baseContext: Context, containing: DeclaredType,
- executableElement: ExecutableElement): UpdateMethod {
- return UpdateMethodProcessor(baseContext, containing, executableElement).process()
- }
-
- @Test
- fun goodConflict() {
- singleShortcutMethod(
- """
- @Update(onConflict = OnConflictStrategy.REPLACE)
- abstract public void foo(User user);
- """) { shortcut, _ ->
- assertThat(shortcut.onConflictStrategy, `is`(OnConflictStrategy.REPLACE))
- }.compilesWithoutError()
- }
-
- @Test
- fun badConflict() {
- singleShortcutMethod(
- """
- @Update(onConflict = -1)
- abstract public void foo(User user);
- """) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/BasicColumnTypeAdaptersTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/BasicColumnTypeAdaptersTest.kt
deleted file mode 100644
index 024099b..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/BasicColumnTypeAdaptersTest.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver
-
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.testing.TestInvocation
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import simpleRun
-import testCodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.PrimitiveType
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-
-@RunWith(Parameterized::class)
-class BasicColumnTypeAdaptersTest(val input: Input, val bindCode: String,
- val cursorCode: String) {
- val scope = testCodeGenScope()
-
- companion object {
- val SQLITE_STMT: TypeName = ClassName.get("android.database.sqlite", "SQLiteStatement")
- val CURSOR: TypeName = ClassName.get("android.database", "Cursor")
-
- @Parameterized.Parameters(name = "kind:{0},bind:_{1},cursor:_{2}")
- @JvmStatic
- fun params(): List<Array<Any>> {
- return listOf(
- arrayOf(Input(TypeKind.INT),
- "st.bindLong(6, inp);",
- "out = crs.getInt(9);"),
- arrayOf(Input(TypeKind.BYTE),
- "st.bindLong(6, inp);",
- "out = (byte) crs.getShort(9);"),
- arrayOf(Input(TypeKind.SHORT),
- "st.bindLong(6, inp);",
- "out = crs.getShort(9);"),
- arrayOf(Input(TypeKind.LONG),
- "st.bindLong(6, inp);",
- "out = crs.getLong(9);"),
- arrayOf(Input(TypeKind.CHAR),
- "st.bindLong(6, inp);",
- "out = (char) crs.getInt(9);"),
- arrayOf(Input(TypeKind.FLOAT),
- "st.bindDouble(6, inp);",
- "out = crs.getFloat(9);"),
- arrayOf(Input(TypeKind.DOUBLE),
- "st.bindDouble(6, inp);",
- "out = crs.getDouble(9);"),
- arrayOf(Input(TypeKind.DECLARED, "java.lang.String"),
- """
- if (inp == null) {
- st.bindNull(6);
- } else {
- st.bindString(6, inp);
- }
- """.trimIndent(),
- "out = crs.getString(9);")
- )
- }
- }
-
- @Test
- fun bind() {
- simpleRun { invocation ->
- val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
- .findColumnTypeAdapter(input.getTypeMirror(invocation.processingEnv), null)!!
- adapter.bindToStmt("st", "6", "inp", scope)
- assertThat(scope.generate().trim(), `is`(bindCode))
- generateCode(invocation, false)
- }.compilesWithoutError()
- }
-
- @Test
- fun boxedBind() {
- if (!input.typeKind.isPrimitive) {
- return // no-op for those
- }
- simpleRun { invocation ->
- val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
- .findColumnTypeAdapter(
- input.getBoxedTypeMirror(invocation.processingEnv), null)!!
- adapter.bindToStmt("st", "6", "inp", scope)
- assertThat(scope.generate().trim(), `is`(
- """
- if (inp == null) {
- st.bindNull(6);
- } else {
- $bindCode
- }
- """.trimIndent()
- ))
- generateCode(invocation, true)
- }.compilesWithoutError()
- }
-
- private fun generateCode(invocation: TestInvocation, boxed: Boolean) {
- val typeMirror = if (boxed) input.getBoxedTypeMirror(invocation.processingEnv)
- else input.getTypeMirror(invocation.processingEnv)
- val spec = TypeSpec.classBuilder("OutClass")
- .addField(FieldSpec.builder(SQLITE_STMT, "st").build())
- .addField(FieldSpec.builder(CURSOR, "crs").build())
- .addField(FieldSpec.builder(TypeName.get(typeMirror), "out").build())
- .addField(FieldSpec.builder(TypeName.get(typeMirror), "inp").build())
- .addMethod(
- MethodSpec.methodBuilder("foo")
- .addCode(scope.builder().build())
- .build()
- )
- .build()
- JavaFile.builder("foo.bar", spec).build().writeTo(invocation.processingEnv.filer)
- }
-
- @Test
- fun read() {
- simpleRun { invocation ->
- val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
- .findColumnTypeAdapter(input.getTypeMirror(invocation.processingEnv), null)!!
- adapter.readFromCursor("out", "crs", "9", scope)
- assertThat(scope.generate().trim(), `is`(cursorCode))
- generateCode(invocation, false)
- }.compilesWithoutError()
- }
-
- @Test
- fun readBoxed() {
- if (!input.typeKind.isPrimitive) {
- return // no-op for those
- }
- simpleRun { invocation ->
- val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
- .findColumnTypeAdapter(
- input.getBoxedTypeMirror(invocation.processingEnv), null)!!
- adapter.readFromCursor("out", "crs", "9", scope)
- assertThat(scope.generate().trim(), `is`(
- """
- if (crs.isNull(9)) {
- out = null;
- } else {
- $cursorCode
- }
- """.trimIndent()
- ))
- generateCode(invocation, true)
- }.compilesWithoutError()
- }
-
- data class Input(val typeKind: TypeKind, val qName: String? = null) {
- fun getTypeMirror(processingEnv: ProcessingEnvironment): TypeMirror {
- return if (typeKind.isPrimitive) {
- processingEnv.typeUtils.getPrimitiveType(typeKind)
- } else {
- processingEnv.elementUtils.getTypeElement(qName).asType()
- }
- }
-
- fun getBoxedTypeMirror(processingEnv: ProcessingEnvironment): TypeMirror {
- return if (typeKind.isPrimitive) {
- processingEnv.typeUtils
- .boxedClass(getTypeMirror(processingEnv) as PrimitiveType)
- .asType()
- } else {
- getTypeMirror(processingEnv)
- }
- }
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
deleted file mode 100644
index ae635b2..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * 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.
- */
-
-@file:Suppress("HasPlatformType")
-
-package android.arch.persistence.room.solver
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Database
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.RoomProcessor
-import android.arch.persistence.room.TypeConverter
-import android.arch.persistence.room.TypeConverters
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.ext.S
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.AnnotationSpec
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.lang.model.element.Modifier
-import javax.tools.JavaFileObject
-
-@RunWith(JUnit4::class)
-class CustomTypeConverterResolutionTest {
- fun TypeSpec.toJFO(): JavaFileObject {
- return JavaFileObjects.forSourceString("foo.bar.${this.name}",
- "package foo.bar;\n" + toString())
- }
-
- companion object {
- val ENTITY = ClassName.get("foo.bar", "MyEntity")
- val DB = ClassName.get("foo.bar", "MyDb")
- val DAO = ClassName.get("foo.bar", "MyDao")
-
- val CUSTOM_TYPE = ClassName.get("foo.bar", "CustomType")
- val CUSTOM_TYPE_JFO = JavaFileObjects.forSourceLines(CUSTOM_TYPE.toString(),
- """
- package ${CUSTOM_TYPE.packageName()};
- public class ${CUSTOM_TYPE.simpleName()} {
- public int value;
- }
- """)
- val CUSTOM_TYPE_CONVERTER = ClassName.get("foo.bar", "MyConverter")
- val CUSTOM_TYPE_CONVERTER_JFO = JavaFileObjects.forSourceLines(
- CUSTOM_TYPE_CONVERTER.toString(),
- """
- package ${CUSTOM_TYPE_CONVERTER.packageName()};
- public class ${CUSTOM_TYPE_CONVERTER.simpleName()} {
- @${TypeConverter::class.java.canonicalName}
- public static $CUSTOM_TYPE toCustom(int value) {
- return null;
- }
- @${TypeConverter::class.java.canonicalName}
- public static int fromCustom($CUSTOM_TYPE input) {
- return 0;
- }
- }
- """)
- val CUSTOM_TYPE_SET = ParameterizedTypeName.get(
- ClassName.get(Set::class.java), CUSTOM_TYPE)
- val CUSTOM_TYPE_SET_CONVERTER = ClassName.get("foo.bar", "MySetConverter")
- val CUSTOM_TYPE_SET_CONVERTER_JFO = JavaFileObjects.forSourceLines(
- CUSTOM_TYPE_SET_CONVERTER.toString(),
- """
- package ${CUSTOM_TYPE_SET_CONVERTER.packageName()};
- import java.util.HashSet;
- import java.util.Set;
- public class ${CUSTOM_TYPE_SET_CONVERTER.simpleName()} {
- @${TypeConverter::class.java.canonicalName}
- public static $CUSTOM_TYPE_SET toCustom(int value) {
- return null;
- }
- @${TypeConverter::class.java.canonicalName}
- public static int fromCustom($CUSTOM_TYPE_SET input) {
- return 0;
- }
- }
- """)
- }
-
- @Test
- fun useFromDatabase_forEntity() {
- val entity = createEntity(hasCustomField = true)
- val database = createDatabase(hasConverters = true, hasDao = true)
- val dao = createDao(hasQueryReturningEntity = true, hasQueryWithCustomParam = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun collection_forEntity() {
- val entity = createEntity(
- hasCustomField = true,
- useCollection = true)
- val database = createDatabase(
- hasConverters = true,
- hasDao = true,
- useCollection = true)
- val dao = createDao(
- hasQueryWithCustomParam = false,
- useCollection = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun collection_forDao() {
- val entity = createEntity(
- hasCustomField = true,
- useCollection = true)
- val database = createDatabase(
- hasConverters = true,
- hasDao = true,
- useCollection = true)
- val dao = createDao(
- hasQueryWithCustomParam = true,
- useCollection = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromDatabase_forQueryParameter() {
- val entity = createEntity()
- val database = createDatabase(hasConverters = true, hasDao = true)
- val dao = createDao(hasQueryWithCustomParam = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromDatabase_forReturnValue() {
- val entity = createEntity(hasCustomField = true)
- val database = createDatabase(hasConverters = true, hasDao = true)
- val dao = createDao(hasQueryReturningEntity = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromDao_forQueryParameter() {
- val entity = createEntity()
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasConverters = true, hasQueryReturningEntity = true,
- hasQueryWithCustomParam = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromEntity_forReturnValue() {
- val entity = createEntity(hasCustomField = true, hasConverters = true)
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasQueryReturningEntity = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromEntityField_forReturnValue() {
- val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasQueryReturningEntity = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromEntity_forQueryParameter() {
- val entity = createEntity(hasCustomField = true, hasConverters = true)
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasQueryWithCustomParam = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO())
- .failsToCompile().withErrorContaining(CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
- }
-
- @Test
- fun useFromEntityField_forQueryParameter() {
- val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasQueryWithCustomParam = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO())
- .failsToCompile().withErrorContaining(CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
- }
-
- @Test
- fun useFromQueryMethod_forQueryParameter() {
- val entity = createEntity()
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasQueryWithCustomParam = true, hasMethodConverters = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- @Test
- fun useFromQueryParameter_forQueryParameter() {
- val entity = createEntity()
- val database = createDatabase(hasDao = true)
- val dao = createDao(hasQueryWithCustomParam = true, hasParameterConverters = true)
- run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
- }
-
- fun run(vararg jfos: JavaFileObject): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO
- + CUSTOM_TYPE_SET_CONVERTER_JFO)
- .processedWith(RoomProcessor())
- }
-
- private fun createEntity(
- hasCustomField: Boolean = false,
- hasConverters: Boolean = false,
- hasConverterOnField: Boolean = false,
- useCollection: Boolean = false): TypeSpec {
- if (hasConverterOnField && hasConverters) {
- throw IllegalArgumentException("cannot have both converters")
- }
- val type = if (useCollection) {
- CUSTOM_TYPE_SET
- } else {
- CUSTOM_TYPE
- }
- return TypeSpec.classBuilder(ENTITY).apply {
- addAnnotation(Entity::class.java)
- addModifiers(Modifier.PUBLIC)
- if (hasCustomField) {
- addField(FieldSpec.builder(type, "myCustomField", Modifier.PUBLIC).apply {
- if (hasConverterOnField) {
- addAnnotation(createConvertersAnnotation())
- }
- }.build())
- }
- if (hasConverters) {
- addAnnotation(createConvertersAnnotation())
- }
- addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
- addAnnotation(PrimaryKey::class.java)
- }.build())
- }.build()
- }
-
- private fun createDatabase(
- hasConverters: Boolean = false,
- hasDao: Boolean = false,
- useCollection: Boolean = false): TypeSpec {
- return TypeSpec.classBuilder(DB).apply {
- addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
- superclass(RoomTypeNames.ROOM_DB)
- if (hasConverters) {
- addAnnotation(createConvertersAnnotation(useCollection = useCollection))
- }
- addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
- addAnnotation(PrimaryKey::class.java)
- }.build())
- if (hasDao) {
- addMethod(MethodSpec.methodBuilder("getDao").apply {
- addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
- returns(DAO)
- }.build())
- }
- addAnnotation(
- AnnotationSpec.builder(Database::class.java).apply {
- addMember("entities", "{$T.class}", ENTITY)
- addMember("version", "42")
- }.build()
- )
- }.build()
- }
-
- private fun createDao(
- hasConverters: Boolean = false,
- hasQueryReturningEntity: Boolean = false,
- hasQueryWithCustomParam: Boolean = false,
- hasMethodConverters: Boolean = false,
- hasParameterConverters: Boolean = false,
- useCollection: Boolean = false): TypeSpec {
- val annotationCount = listOf(hasMethodConverters, hasConverters, hasParameterConverters)
- .map { if (it) 1 else 0 }.sum()
- if (annotationCount > 1) {
- throw IllegalArgumentException("cannot set both of these")
- }
- if (hasParameterConverters && !hasQueryWithCustomParam) {
- throw IllegalArgumentException("inconsistent")
- }
- return TypeSpec.classBuilder(DAO).apply {
- addAnnotation(Dao::class.java)
- addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
- if (hasConverters) {
- addAnnotation(createConvertersAnnotation(useCollection = useCollection))
- }
- if (hasQueryReturningEntity) {
- addMethod(MethodSpec.methodBuilder("loadAll").apply {
- addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
- addMember("value", S, "SELECT * FROM ${ENTITY.simpleName()} LIMIT 1")
- }.build())
- addModifiers(Modifier.ABSTRACT)
- returns(ENTITY)
- }.build())
- }
- val customType = if (useCollection) {
- CUSTOM_TYPE_SET
- } else {
- CUSTOM_TYPE
- }
- if (hasQueryWithCustomParam) {
- addMethod(MethodSpec.methodBuilder("queryWithCustom").apply {
- addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
- addMember("value", S, "SELECT COUNT(*) FROM ${ENTITY.simpleName()} where" +
- " id = :custom")
- }.build())
- if (hasMethodConverters) {
- addAnnotation(createConvertersAnnotation(useCollection = useCollection))
- }
- addParameter(ParameterSpec.builder(customType, "custom").apply {
- if (hasParameterConverters) {
- addAnnotation(createConvertersAnnotation(useCollection = useCollection))
- }
- }.build())
- addModifiers(Modifier.ABSTRACT)
- returns(TypeName.INT)
- }.build())
- }
- }.build()
- }
-
- private fun createConvertersAnnotation(useCollection: Boolean = false): AnnotationSpec {
- val converter = if (useCollection) {
- CUSTOM_TYPE_SET_CONVERTER
- } else {
- CUSTOM_TYPE_CONVERTER
- }
- return AnnotationSpec.builder(TypeConverters::class.java)
- .addMember("value", "$T.class", converter).build()
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAdapterStoreTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAdapterStoreTest.kt
deleted file mode 100644
index 7ad370e..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAdapterStoreTest.kt
+++ /dev/null
@@ -1,427 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver
-
-import COMMON
-import android.arch.paging.DataSource
-import android.arch.paging.PositionalDataSource
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.ext.L
-import android.arch.persistence.room.ext.LifecyclesTypeNames
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.ext.ReactiveStreamsTypeNames
-import android.arch.persistence.room.ext.RoomTypeNames.STRING_UTIL
-import android.arch.persistence.room.ext.RxJava2TypeNames
-import android.arch.persistence.room.ext.T
-import android.arch.persistence.room.ext.typeName
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.processor.ProcessorErrors
-import android.arch.persistence.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.DataSourceQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.FlowableQueryResultBinderProvider
-import android.arch.persistence.room.solver.binderprovider.LiveDataQueryResultBinderProvider
-import android.arch.persistence.room.solver.types.CompositeAdapter
-import android.arch.persistence.room.solver.types.TypeConverter
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.TypeName
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.instanceOf
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import simpleRun
-import testCodeGenScope
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.type.TypeKind
-
-@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-@RunWith(JUnit4::class)
-class TypeAdapterStoreTest {
- companion object {
- fun tmp(index: Int) = CodeGenScope._tmpVar(index)
- }
-
- @Test
- fun testDirect() {
- singleRun { invocation ->
- val store = TypeAdapterStore.create(Context(invocation.processingEnv))
- val primitiveType = invocation.processingEnv.typeUtils.getPrimitiveType(TypeKind.INT)
- val adapter = store.findColumnTypeAdapter(primitiveType, null)
- assertThat(adapter, notNullValue())
- }.compilesWithoutError()
- }
-
- @Test
- fun testJavaLangBoolean() {
- singleRun { invocation ->
- val store = TypeAdapterStore.create(Context(invocation.processingEnv))
- val boolean = invocation
- .processingEnv
- .elementUtils
- .getTypeElement("java.lang.Boolean")
- .asType()
- val adapter = store.findColumnTypeAdapter(boolean, null)
- assertThat(adapter, notNullValue())
- assertThat(adapter, instanceOf(CompositeAdapter::class.java))
- val composite = adapter as CompositeAdapter
- assertThat(composite.intoStatementConverter?.from?.typeName(),
- `is`(TypeName.BOOLEAN.box()))
- assertThat(composite.columnTypeAdapter.out.typeName(),
- `is`(TypeName.INT.box()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testVia1TypeAdapter() {
- singleRun { invocation ->
- val store = TypeAdapterStore.create(Context(invocation.processingEnv))
- val booleanType = invocation.processingEnv.typeUtils
- .getPrimitiveType(TypeKind.BOOLEAN)
- val adapter = store.findColumnTypeAdapter(booleanType, null)
- assertThat(adapter, notNullValue())
- assertThat(adapter, instanceOf(CompositeAdapter::class.java))
- val bindScope = testCodeGenScope()
- adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
- assertThat(bindScope.generate().trim(), `is`("""
- final int ${tmp(0)};
- ${tmp(0)} = fooVar ? 1 : 0;
- stmt.bindLong(41, ${tmp(0)});
- """.trimIndent()))
-
- val cursorScope = testCodeGenScope()
- adapter.readFromCursor("res", "curs", "7", cursorScope)
- assertThat(cursorScope.generate().trim(), `is`("""
- final int ${tmp(0)};
- ${tmp(0)} = curs.getInt(7);
- res = ${tmp(0)} != 0;
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testVia2TypeAdapters() {
- singleRun { invocation ->
- val store = TypeAdapterStore.create(Context(invocation.processingEnv),
- pointTypeConverters(invocation.processingEnv))
- val pointType = invocation.processingEnv.elementUtils
- .getTypeElement("foo.bar.Point").asType()
- val adapter = store.findColumnTypeAdapter(pointType, null)
- assertThat(adapter, notNullValue())
- assertThat(adapter, instanceOf(CompositeAdapter::class.java))
-
- val bindScope = testCodeGenScope()
- adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
- assertThat(bindScope.generate().trim(), `is`("""
- final int ${tmp(0)};
- final boolean ${tmp(1)};
- ${tmp(1)} = foo.bar.Point.toBoolean(fooVar);
- ${tmp(0)} = ${tmp(1)} ? 1 : 0;
- stmt.bindLong(41, ${tmp(0)});
- """.trimIndent()))
-
- val cursorScope = testCodeGenScope()
- adapter.readFromCursor("res", "curs", "11", cursorScope).toString()
- assertThat(cursorScope.generate().trim(), `is`("""
- final int ${tmp(0)};
- ${tmp(0)} = curs.getInt(11);
- final boolean ${tmp(1)};
- ${tmp(1)} = ${tmp(0)} != 0;
- res = foo.bar.Point.fromBoolean(${tmp(1)});
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testDate() {
- singleRun { (processingEnv) ->
- val store = TypeAdapterStore.create(Context(processingEnv),
- dateTypeConverters(processingEnv))
- val tDate = processingEnv.elementUtils.getTypeElement("java.util.Date").asType()
- val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER)
- assertThat(adapter, notNullValue())
- assertThat(adapter?.typeMirror(), `is`(tDate))
- val bindScope = testCodeGenScope()
- adapter!!.readFromCursor("outDate", "curs", "0", bindScope)
- assertThat(bindScope.generate().trim(), `is`("""
- final java.lang.Long _tmp;
- if (curs.isNull(0)) {
- _tmp = null;
- } else {
- _tmp = curs.getLong(0);
- }
- // convert Long to Date;
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testIntList() {
- singleRun { invocation ->
- val binders = createIntListToStringBinders(invocation)
- val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0],
- binders[1])
-
- val adapter = store.findColumnTypeAdapter(binders[0].from, null)
- assertThat(adapter, notNullValue())
-
- val bindScope = testCodeGenScope()
- adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
- assertThat(bindScope.generate().trim(), `is`("""
- final java.lang.String ${tmp(0)};
- ${tmp(0)} = android.arch.persistence.room.util.StringUtil.joinIntoString(fooVar);
- if (${tmp(0)} == null) {
- stmt.bindNull(41);
- } else {
- stmt.bindString(41, ${tmp(0)});
- }
- """.trimIndent()))
-
- val converter = store.findTypeConverter(binders[0].from,
- invocation.context.COMMON_TYPES.STRING)
- assertThat(converter, notNullValue())
- assertThat(store.reverse(converter!!), `is`(binders[1]))
- }.compilesWithoutError()
- }
-
- @Test
- fun testOneWayConversion() {
- singleRun { invocation ->
- val binders = createIntListToStringBinders(invocation)
- val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0])
- val adapter = store.findColumnTypeAdapter(binders[0].from, null)
- assertThat(adapter, nullValue())
-
- val stmtBinder = store.findStatementValueBinder(binders[0].from, null)
- assertThat(stmtBinder, notNullValue())
-
- val converter = store.findTypeConverter(binders[0].from,
- invocation.context.COMMON_TYPES.STRING)
- assertThat(converter, notNullValue())
- assertThat(store.reverse(converter!!), nullValue())
- }
- }
-
- @Test
- fun testMissingRxRoom() {
- simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE)) { invocation ->
- val publisherElement = invocation.processingEnv.elementUtils
- .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString())
- assertThat(publisherElement, notNullValue())
- assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(publisherElement.asType())), `is`(true))
- }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
- }
-
- @Test
- fun testFindPublisher() {
- simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) {
- invocation ->
- val publisher = invocation.processingEnv.elementUtils
- .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString())
- assertThat(publisher, notNullValue())
- assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(publisher.asType())), `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun testFindFlowable() {
- simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) {
- invocation ->
- val flowable = invocation.processingEnv.elementUtils
- .getTypeElement(RxJava2TypeNames.FLOWABLE.toString())
- assertThat(flowable, notNullValue())
- assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(flowable.asType())), `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun testFindLiveData() {
- simpleRun(jfos = *arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) {
- invocation ->
- val liveData = invocation.processingEnv.elementUtils
- .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString())
- assertThat(liveData, notNullValue())
- assertThat(LiveDataQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(liveData.asType())), `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun findDataSource() {
- simpleRun {
- invocation ->
- val dataSource = invocation.processingEnv.elementUtils
- .getTypeElement(DataSource::class.java.canonicalName)
- assertThat(dataSource, notNullValue())
- assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(dataSource.asType())), `is`(true))
- }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
- }
-
- @Test
- fun findPositionalDataSource() {
- simpleRun {
- invocation ->
- val dataSource = invocation.processingEnv.elementUtils
- .getTypeElement(PositionalDataSource::class.java.canonicalName)
- assertThat(dataSource, notNullValue())
- assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(dataSource.asType())), `is`(true))
- }.compilesWithoutError()
- }
-
- @Test
- fun findDataSourceFactory() {
- simpleRun(jfos = COMMON.DATA_SOURCE_FACTORY) {
- invocation ->
- val pagedListProvider = invocation.processingEnv.elementUtils
- .getTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY.toString())
- assertThat(pagedListProvider, notNullValue())
- assertThat(DataSourceFactoryQueryResultBinderProvider(invocation.context).matches(
- MoreTypes.asDeclared(pagedListProvider.asType())), `is`(true))
- }.compilesWithoutError()
- }
-
- private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> {
- val intType = invocation.processingEnv.elementUtils
- .getTypeElement(Integer::class.java.canonicalName)
- .asType()
- val listType = invocation.processingEnv.elementUtils
- .getTypeElement(java.util.List::class.java.canonicalName)
- val listOfInts = invocation.processingEnv.typeUtils.getDeclaredType(listType, intType)
-
- val intListConverter = object : TypeConverter(listOfInts,
- invocation.context.COMMON_TYPES.STRING) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("$L = $T.joinIntoString($L)", outputVarName, STRING_UTIL,
- inputVarName)
- }
- }
- }
-
- val stringToIntListConverter = object : TypeConverter(
- invocation.context.COMMON_TYPES.STRING, listOfInts) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("$L = $T.splitToIntList($L)", outputVarName, STRING_UTIL,
- inputVarName)
- }
- }
- }
- return listOf(intListConverter, stringToIntListConverter)
- }
-
- fun singleRun(handler: (TestInvocation) -> Unit): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.DummyClass",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- public class DummyClass {}
- """
- ), JavaFileObjects.forSourceString("foo.bar.Point",
- """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Entity
- public class Point {
- public int x, y;
- public Point(int x, int y) {
- this.x = x;
- this.y = y;
- }
- public static Point fromBoolean(boolean val) {
- return val ? new Point(1, 1) : new Point(0, 0);
- }
- public static boolean toBoolean(Point point) {
- return point.x > 0;
- }
- }
- """
- )))
- .processedWith(TestProcessor.builder()
- .forAnnotations(Entity::class)
- .nextRunHandler { invocation ->
- handler(invocation)
- true
- }
- .build())
- }
-
- fun pointTypeConverters(env: ProcessingEnvironment): List<TypeConverter> {
- val tPoint = env.elementUtils.getTypeElement("foo.bar.Point").asType()
- val tBoolean = env.typeUtils.getPrimitiveType(TypeKind.BOOLEAN)
- return listOf(
- object : TypeConverter(tPoint, tBoolean) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("$L = $T.toBoolean($L)", outputVarName, from, inputVarName)
- }
- }
- },
- object : TypeConverter(tBoolean, tPoint) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("$L = $T.fromBoolean($L)", outputVarName, tPoint,
- inputVarName)
- }
- }
- }
- )
- }
-
- fun dateTypeConverters(env: ProcessingEnvironment): List<TypeConverter> {
- val tDate = env.elementUtils.getTypeElement("java.util.Date").asType()
- val tLong = env.elementUtils.getTypeElement("java.lang.Long").asType()
- return listOf(
- object : TypeConverter(tDate, tLong) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("// convert Date to Long")
- }
- }
- },
- object : TypeConverter(tLong, tDate) {
- override fun convert(inputVarName: String, outputVarName: String,
- scope: CodeGenScope) {
- scope.builder().apply {
- addStatement("// convert Long to Date")
- }
- }
- }
- )
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt
deleted file mode 100644
index 03d4031..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright 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.solver
-
-import android.arch.persistence.room.ext.getAllFieldsIncludingPrivateSupers
-import android.arch.persistence.room.ext.isAssignableWithoutVariance
-import android.arch.persistence.room.testing.TestInvocation
-import com.google.testing.compile.JavaFileObjects
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import simpleRun
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.TypeElement
-import javax.lang.model.element.VariableElement
-
-class TypeAssignmentTest {
- companion object {
- private val TEST_OBJECT = JavaFileObjects.forSourceString("foo.bar.MyObject",
- """
- package foo.bar;
- import java.util.Set;
- import java.util.HashSet;
- import java.util.Map;
- class MyObject {
- String mString;
- Integer mInteger;
- Set<MyObject> mSet;
- Set<? extends MyObject> mVarianceSet;
- HashSet<MyObject> mHashSet;
- Map<String, ?> mUnboundedMap;
- Map<String, String> mStringMap;
- }
- """.trimIndent())
- }
-
- @Test
- fun basic() {
- runTest {
- val testObject = typeElement("foo.bar.MyObject")
- val string = testObject.getField(processingEnv, "mString")
- val integer = testObject.getField(processingEnv, "mInteger")
- assertThat(typeUtils.isAssignableWithoutVariance(string.asType(),
- integer.asType()),
- `is`(false))
- }
- }
-
- @Test
- fun generics() {
- runTest {
- val testObject = typeElement("foo.bar.MyObject")
- val set = testObject.getField(processingEnv, "mSet").asType()
- val hashSet = testObject.getField(processingEnv, "mHashSet").asType()
- assertThat(typeUtils.isAssignableWithoutVariance(
- from = set,
- to = hashSet
- ), `is`(false))
- assertThat(typeUtils.isAssignableWithoutVariance(
- from = hashSet,
- to = set
- ), `is`(true))
- }
- }
-
- @Test
- fun variance() {
- /**
- * Set<User> userSet = null;
- * Set<? extends User> userSet2 = null;
- * userSet = userSet2; // NOT OK for java but kotlin data classes hit this so we want
- * // to accept it
- */
- runTest {
- val testObject = typeElement("foo.bar.MyObject")
- val set = testObject.getField(processingEnv, "mSet").asType()
- val varianceSet = testObject.getField(processingEnv, "mVarianceSet").asType()
- assertThat(typeUtils.isAssignableWithoutVariance(
- from = set,
- to = varianceSet
- ), `is`(true))
- assertThat(typeUtils.isAssignableWithoutVariance(
- from = varianceSet,
- to = set
- ), `is`(true))
- }
- }
-
- @Test
- fun unboundedVariance() {
- runTest {
- val testObject = typeElement("foo.bar.MyObject")
- val unbounded = testObject.getField(processingEnv, "mUnboundedMap").asType()
- val objectMap = testObject.getField(processingEnv, "mStringMap").asType()
- assertThat(typeUtils.isAssignableWithoutVariance(
- from = unbounded,
- to = objectMap
- ), `is`(false))
- assertThat(typeUtils.isAssignableWithoutVariance(
- from = objectMap,
- to = unbounded
- ), `is`(true))
- }
- }
-
- private fun TypeElement.getField(
- env: ProcessingEnvironment,
- name: String): VariableElement {
- return getAllFieldsIncludingPrivateSupers(env).first {
- it.simpleName.toString() == name
- }
- }
-
- private fun runTest(handler: TestInvocation.() -> Unit) {
- simpleRun(TEST_OBJECT) {
- it.apply { handler() }
- }.compilesWithoutError()
- }
-}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/query/QueryWriterTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/query/QueryWriterTest.kt
deleted file mode 100644
index 6401d96..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/query/QueryWriterTest.kt
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Copyright (C) 2016 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.solver.query
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.ext.RoomTypeNames.ROOM_SQL_QUERY
-import android.arch.persistence.room.ext.RoomTypeNames.STRING_UTIL
-import android.arch.persistence.room.processor.QueryMethodProcessor
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.writer.QueryWriter
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourceSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import testCodeGenScope
-
-@RunWith(JUnit4::class)
-class QueryWriterTest {
- companion object {
- const val DAO_PREFIX = """
- package foo.bar;
- import android.arch.persistence.room.*;
- import java.util.*;
- @Dao
- abstract class MyClass {
- """
- const val DAO_SUFFIX = "}"
- val QUERY = ROOM_SQL_QUERY.toString()
- }
-
- @Test
- fun simpleNoArgQuery() {
- singleQueryMethod("""
- @Query("SELECT id FROM users")
- abstract java.util.List<Integer> selectAllIds();
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`("""
- final java.lang.String _sql = "SELECT id FROM users";
- final $QUERY _stmt = $QUERY.acquire(_sql, 0);
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun simpleStringArgs() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE name LIKE :name")
- abstract java.util.List<Integer> selectAllIds(String name);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`(
- """
- final java.lang.String _sql = "SELECT id FROM users WHERE name LIKE ?";
- final $QUERY _stmt = $QUERY.acquire(_sql, 1);
- int _argIndex = 1;
- if (name == null) {
- _stmt.bindNull(_argIndex);
- } else {
- _stmt.bindString(_argIndex, name);
- }
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun twoIntArgs() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE id IN(:id1,:id2)")
- abstract java.util.List<Integer> selectAllIds(int id1, int id2);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`(
- """
- final java.lang.String _sql = "SELECT id FROM users WHERE id IN(?,?)";
- final $QUERY _stmt = $QUERY.acquire(_sql, 2);
- int _argIndex = 1;
- _stmt.bindLong(_argIndex, id1);
- _argIndex = 2;
- _stmt.bindLong(_argIndex, id2);
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun aLongAndIntVarArg() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE id IN(:ids) AND age > :time")
- abstract java.util.List<Integer> selectAllIds(long time, int... ids);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`(
- """
- java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
- _stringBuilder.append("SELECT id FROM users WHERE id IN(");
- final int _inputSize = ids.length;
- $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
- _stringBuilder.append(") AND age > ");
- _stringBuilder.append("?");
- final java.lang.String _sql = _stringBuilder.toString();
- final int _argCount = 1 + _inputSize;
- final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
- int _argIndex = 1;
- for (int _item : ids) {
- _stmt.bindLong(_argIndex, _item);
- _argIndex ++;
- }
- _argIndex = 1 + _inputSize;
- _stmt.bindLong(_argIndex, time);
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- val collectionOut = """
- java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
- _stringBuilder.append("SELECT id FROM users WHERE id IN(");
- final int _inputSize = ids.size();
- $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
- _stringBuilder.append(") AND age > ");
- _stringBuilder.append("?");
- final java.lang.String _sql = _stringBuilder.toString();
- final int _argCount = 1 + _inputSize;
- final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
- int _argIndex = 1;
- for (java.lang.Integer _item : ids) {
- if (_item == null) {
- _stmt.bindNull(_argIndex);
- } else {
- _stmt.bindLong(_argIndex, _item);
- }
- _argIndex ++;
- }
- _argIndex = 1 + _inputSize;
- _stmt.bindLong(_argIndex, time);
- """.trimIndent()
-
- @Test
- fun aLongAndIntegerList() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE id IN(:ids) AND age > :time")
- abstract List<Integer> selectAllIds(long time, List<Integer> ids);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`(collectionOut))
- }.compilesWithoutError()
- }
-
- @Test
- fun aLongAndIntegerSet() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE id IN(:ids) AND age > :time")
- abstract List<Integer> selectAllIds(long time, Set<Integer> ids);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`(collectionOut))
- }.compilesWithoutError()
- }
-
- @Test
- fun testMultipleBindParamsWithSameName() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE age > :age OR bage > :age")
- abstract List<Integer> selectAllIds(int age);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`("""
- final java.lang.String _sql = "SELECT id FROM users WHERE age > ? OR bage > ?";
- final $QUERY _stmt = $QUERY.acquire(_sql, 2);
- int _argIndex = 1;
- _stmt.bindLong(_argIndex, age);
- _argIndex = 2;
- _stmt.bindLong(_argIndex, age);
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testMultipleBindParamsWithSameNameWithVarArg() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE age > :age OR bage > :age OR fage IN(:ages)")
- abstract List<Integer> selectAllIds(int age, int... ages);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`("""
- java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
- _stringBuilder.append("SELECT id FROM users WHERE age > ");
- _stringBuilder.append("?");
- _stringBuilder.append(" OR bage > ");
- _stringBuilder.append("?");
- _stringBuilder.append(" OR fage IN(");
- final int _inputSize = ages.length;
- $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
- _stringBuilder.append(")");
- final java.lang.String _sql = _stringBuilder.toString();
- final int _argCount = 2 + _inputSize;
- final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
- int _argIndex = 1;
- _stmt.bindLong(_argIndex, age);
- _argIndex = 2;
- _stmt.bindLong(_argIndex, age);
- _argIndex = 3;
- for (int _item : ages) {
- _stmt.bindLong(_argIndex, _item);
- _argIndex ++;
- }
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- @Test
- fun testMultipleBindParamsWithSameNameWithVarArgInTwoBindings() {
- singleQueryMethod("""
- @Query("SELECT id FROM users WHERE age IN (:ages) OR bage > :age OR fage IN(:ages)")
- abstract List<Integer> selectAllIds(int age, int... ages);
- """) { writer ->
- val scope = testCodeGenScope()
- writer.prepareReadAndBind("_sql", "_stmt", scope)
- assertThat(scope.generate().trim(), `is`("""
- java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
- _stringBuilder.append("SELECT id FROM users WHERE age IN (");
- final int _inputSize = ages.length;
- $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
- _stringBuilder.append(") OR bage > ");
- _stringBuilder.append("?");
- _stringBuilder.append(" OR fage IN(");
- final int _inputSize_1 = ages.length;
- $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize_1);
- _stringBuilder.append(")");
- final java.lang.String _sql = _stringBuilder.toString();
- final int _argCount = 1 + _inputSize + _inputSize_1;
- final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
- int _argIndex = 1;
- for (int _item : ages) {
- _stmt.bindLong(_argIndex, _item);
- _argIndex ++;
- }
- _argIndex = 1 + _inputSize;
- _stmt.bindLong(_argIndex, age);
- _argIndex = 2 + _inputSize;
- for (int _item_1 : ages) {
- _stmt.bindLong(_argIndex, _item_1);
- _argIndex ++;
- }
- """.trimIndent()))
- }.compilesWithoutError()
- }
-
- fun singleQueryMethod(vararg input: String,
- handler: (QueryWriter) -> Unit):
- CompileTester {
- return Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
- .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
- DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
- ))
- .processedWith(TestProcessor.builder()
- .forAnnotations(Query::class, Dao::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.java)
- .map {
- Pair(it,
- invocation.processingEnv.elementUtils
- .getAllMembers(MoreElements.asType(it))
- .filter {
- MoreElements.isAnnotationPresent(it,
- Query::class.java)
- }
- )
- }.filter { it.second.isNotEmpty() }.first()
- val parser = QueryMethodProcessor(
- baseContext = invocation.context,
- containing = MoreTypes.asDeclared(owner.asType()),
- executableElement = MoreElements.asExecutable(methods.first()))
- val parsedQuery = parser.process()
- handler(QueryWriter(parsedQuery))
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/InProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/InProcessorTest.kt
deleted file mode 100644
index 31e1209..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/InProcessorTest.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2016 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.testing
-
-import android.arch.persistence.room.Query
-import com.google.common.truth.Truth
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourceSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import java.util.concurrent.atomic.AtomicBoolean
-
-@RunWith(JUnit4::class)
-class InProcessorTest {
- @Test
- fun testInProcessorTestRuns() {
- val didRun = AtomicBoolean(false)
- Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
- .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
- """
- package foo.bar;
- abstract public class MyClass {
- @android.arch.persistence.room.Query("foo")
- abstract public void setFoo(String foo);
- }
- """))
- .processedWith(TestProcessor.builder()
- .nextRunHandler { invocation ->
- didRun.set(true)
- assertThat(invocation.annotations.size, `is`(1))
- true
- }
- .forAnnotations(Query::class)
- .build())
- .compilesWithoutError()
- assertThat(didRun.get(), `is`(true))
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
deleted file mode 100644
index e03f1db..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2016 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.testing
-
-import android.arch.persistence.room.processor.Context
-import javax.annotation.processing.ProcessingEnvironment
-import javax.annotation.processing.RoundEnvironment
-import javax.lang.model.element.TypeElement
-
-data class TestInvocation(val processingEnv: ProcessingEnvironment,
- val annotations: MutableSet<out TypeElement>,
- val roundEnv: RoundEnvironment) {
- val context = Context(processingEnv)
-
- fun typeElement(qName: String): TypeElement {
- return processingEnv.elementUtils.getTypeElement(qName)
- }
-
- val typeUtils by lazy { processingEnv.typeUtils }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestProcessor.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestProcessor.kt
deleted file mode 100644
index 6dfa2e4..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestProcessor.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 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.testing
-
-import javax.annotation.processing.AbstractProcessor
-import javax.annotation.processing.RoundEnvironment
-import javax.annotation.processing.SupportedSourceVersion
-import javax.lang.model.SourceVersion
-import javax.lang.model.element.TypeElement
-import kotlin.reflect.KClass
-
-@SupportedSourceVersion(SourceVersion.RELEASE_8)// test are compiled w/ J_8
-class TestProcessor(val handlers: List<(TestInvocation) -> Boolean>,
- val annotations: MutableSet<String>) : AbstractProcessor() {
- var count = 0
- override fun process(
- annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
- return handlers.getOrNull(count++)?.invoke(
- TestInvocation(processingEnv, annotations, roundEnv)) ?: true
- }
-
- override fun getSupportedAnnotationTypes(): MutableSet<String> {
- return annotations
- }
-
- class Builder {
- private var handlers = arrayListOf<(TestInvocation) -> Boolean>()
- private var annotations = mutableSetOf<String>()
- fun nextRunHandler(f: (TestInvocation) -> Boolean): Builder {
- handlers.add(f)
- return this
- }
-
- fun forAnnotations(vararg klasses: KClass<*>): Builder {
- annotations.addAll(klasses.map { it.java.canonicalName })
- return this
- }
-
- fun build(): TestProcessor {
- if (annotations.isEmpty()) {
- throw IllegalStateException("must provide at least 1 annotation")
- }
- if (handlers.isEmpty()) {
- throw IllegalStateException("must provide at least 1 handler")
- }
- return TestProcessor(handlers, annotations)
- }
- }
-
- companion object {
- fun builder(): Builder = Builder()
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt
deleted file mode 100644
index c0167b3..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2016 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.
- */
-
-import android.arch.persistence.room.ColumnInfo
-import android.arch.persistence.room.Embedded
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.Relation
-import android.arch.persistence.room.ext.LifecyclesTypeNames
-import android.arch.persistence.room.ext.PagingTypeNames
-import android.arch.persistence.room.ext.ReactiveStreamsTypeNames
-import android.arch.persistence.room.ext.RoomRxJava2TypeNames
-import android.arch.persistence.room.ext.RxJava2TypeNames
-import android.arch.persistence.room.processor.EntityProcessor
-import android.arch.persistence.room.solver.CodeGenScope
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.verifier.DatabaseVerifier
-import android.arch.persistence.room.writer.ClassWriter
-import com.google.auto.common.MoreElements
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import com.squareup.javapoet.ClassName
-import org.mockito.Mockito
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import java.io.File
-import javax.lang.model.element.Element
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-import javax.tools.JavaFileObject
-
-object COMMON {
- val USER by lazy {
- loadJavaCode("common/input/User.java", "foo.bar.User")
- }
- val USER_TYPE_NAME by lazy {
- ClassName.get("foo.bar", "User")
- }
- val BOOK by lazy {
- loadJavaCode("common/input/Book.java", "foo.bar.Book")
- }
- val NOT_AN_ENTITY by lazy {
- loadJavaCode("common/input/NotAnEntity.java", "foo.bar.NotAnEntity")
- }
-
- val NOT_AN_ENTITY_TYPE_NAME by lazy {
- ClassName.get("foo.bar", "NotAnEntity")
- }
-
- val MULTI_PKEY_ENTITY by lazy {
- loadJavaCode("common/input/MultiPKeyEntity.java", "MultiPKeyEntity")
- }
- val LIVE_DATA by lazy {
- loadJavaCode("common/input/LiveData.java", LifecyclesTypeNames.LIVE_DATA.toString())
- }
- val COMPUTABLE_LIVE_DATA by lazy {
- loadJavaCode("common/input/ComputableLiveData.java",
- LifecyclesTypeNames.COMPUTABLE_LIVE_DATA.toString())
- }
- val PUBLISHER by lazy {
- loadJavaCode("common/input/reactivestreams/Publisher.java",
- ReactiveStreamsTypeNames.PUBLISHER.toString())
- }
- val FLOWABLE by lazy {
- loadJavaCode("common/input/rxjava2/Flowable.java", RxJava2TypeNames.FLOWABLE.toString())
- }
-
- val RX2_ROOM by lazy {
- loadJavaCode("common/input/Rx2Room.java", RoomRxJava2TypeNames.RX_ROOM.toString())
- }
-
- val DATA_SOURCE_FACTORY by lazy {
- loadJavaCode("common/input/DataSource.java", "android.arch.paging.DataSource")
- }
-
- val POSITIONAL_DATA_SOURCE by lazy {
- loadJavaCode("common/input/PositionalDataSource.java",
- PagingTypeNames.POSITIONAL_DATA_SOURCE.toString())
- }
-}
-fun testCodeGenScope(): CodeGenScope {
- return CodeGenScope(Mockito.mock(ClassWriter::class.java))
-}
-
-fun simpleRun(vararg jfos: JavaFileObject, f: (TestInvocation) -> Unit): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfos.toList() + JavaFileObjects.forSourceString("foo.bar.MyClass",
- """
- package foo.bar;
- abstract public class MyClass {
- @android.arch.persistence.room.Query("foo")
- abstract public void setFoo(String foo);
- }
- """))
- .processedWith(TestProcessor.builder()
- .nextRunHandler {
- f(it)
- true
- }
- .forAnnotations(Query::class, PrimaryKey::class, Embedded::class,
- ColumnInfo::class, Relation::class, Entity::class)
- .build())
-}
-
-fun loadJavaCode(fileName: String, qName: String): JavaFileObject {
- val contents = File("src/test/data/$fileName").readText(Charsets.UTF_8)
- return JavaFileObjects.forSourceString(qName, contents)
-}
-
-fun createVerifierFromEntities(invocation: TestInvocation): DatabaseVerifier {
- val entities = invocation.roundEnv.getElementsAnnotatedWith(Entity::class.java).map {
- EntityProcessor(invocation.context, MoreElements.asType(it)).process()
- }
- return DatabaseVerifier.create(invocation.context, Mockito.mock(Element::class.java),
- entities)!!
-}
-
-/**
- * Create mocks of [Element] and [TypeMirror] so that they can be used for instantiating a fake
- * [android.arch.persistence.room.vo.Field].
- */
-fun mockElementAndType(): Pair<Element, TypeMirror> {
- val element = mock(Element::class.java)
- val type = mock(TypeMirror::class.java)
- doReturn(TypeKind.DECLARED).`when`(type).kind
- doReturn(type).`when`(element).asType()
- return element to type
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/verifier/DatabaseVerifierTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/verifier/DatabaseVerifierTest.kt
deleted file mode 100644
index f368bb2..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/verifier/DatabaseVerifierTest.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * 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.verifier
-
-import android.arch.persistence.room.parser.Collate
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import android.arch.persistence.room.processor.Context
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.vo.CallType
-import android.arch.persistence.room.vo.Constructor
-import android.arch.persistence.room.vo.Database
-import android.arch.persistence.room.vo.Entity
-import android.arch.persistence.room.vo.Field
-import android.arch.persistence.room.vo.FieldGetter
-import android.arch.persistence.room.vo.FieldSetter
-import android.arch.persistence.room.vo.PrimaryKey
-import collect
-import columnNames
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.hasItem
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import simpleRun
-import java.sql.Connection
-import javax.lang.model.element.Element
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-import javax.lang.model.type.PrimitiveType
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-
-@RunWith(Parameterized::class)
-class DatabaseVerifierTest(private val useLocalizedCollation: Boolean) {
- @Test
- fun testSimpleDatabase() {
- simpleRun { invocation ->
- val verifier = createVerifier(invocation)
- val stmt = verifier.connection.createStatement()
- val rs = stmt.executeQuery("select * from sqlite_master WHERE type='table'")
- assertThat(
- rs.collect { set -> set.getString("name") }, hasItem(`is`("User")))
- val table = verifier.connection.prepareStatement("select * from User")
- assertThat(table.columnNames(), `is`(listOf("id", "name", "lastName", "ratio")))
-
- assertThat(getPrimaryKeys(verifier.connection, "User"), `is`(listOf("id")))
- }.compilesWithoutError()
- }
-
- private fun createVerifier(invocation: TestInvocation): DatabaseVerifier {
- return DatabaseVerifier.create(invocation.context, mock(Element::class.java),
- userDb(invocation.context).entities)!!
- }
-
- @Test
- fun testFullEntityQuery() {
- validQueryTest("select * from User") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- ColumnInfo("id", SQLTypeAffinity.INTEGER),
- ColumnInfo("name", SQLTypeAffinity.TEXT),
- ColumnInfo("lastName", SQLTypeAffinity.TEXT),
- ColumnInfo("ratio", SQLTypeAffinity.REAL)
- ))))
- }
- }
-
- @Test
- fun testPartialFields() {
- validQueryTest("select id, lastName from User") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- ColumnInfo("id", SQLTypeAffinity.INTEGER),
- ColumnInfo("lastName", SQLTypeAffinity.TEXT)
- ))))
- }
- }
-
- @Test
- fun testRenamedField() {
- validQueryTest("select id as myId, lastName from User") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- ColumnInfo("myId", SQLTypeAffinity.INTEGER),
- ColumnInfo("lastName", SQLTypeAffinity.TEXT)
- ))))
- }
- }
-
- @Test
- fun testGrouped() {
- validQueryTest("select MAX(ratio) from User GROUP BY name") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- // unfortunately, we don't get this information
- ColumnInfo("MAX(ratio)", SQLTypeAffinity.NULL)
- ))))
- }
- }
-
- @Test
- fun testConcat() {
- validQueryTest("select name || lastName as mergedName from User") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- // unfortunately, we don't get this information
- ColumnInfo("mergedName", SQLTypeAffinity.NULL)
- ))))
- }
- }
-
- @Test
- fun testResultWithArgs() {
- validQueryTest("select id, name || lastName as mergedName from User where name LIKE ?") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- // unfortunately, we don't get this information
- ColumnInfo("id", SQLTypeAffinity.INTEGER),
- ColumnInfo("mergedName", SQLTypeAffinity.NULL)
- ))))
- }
- }
-
- @Test
- fun testDeleteQuery() {
- validQueryTest("delete from User where name LIKE ?") {
- assertThat(it, `is`(QueryResultInfo(emptyList())))
- }
- }
-
- @Test
- fun testUpdateQuery() {
- validQueryTest("update User set name = ? WHERE id = ?") {
- assertThat(it, `is`(QueryResultInfo(emptyList())))
- }
- }
-
- @Test
- fun testBadQuery() {
- simpleRun { invocation ->
- val verifier = createVerifier(invocation)
- val (_, error) = verifier.analyze("select foo from User")
- assertThat(error, notNullValue())
- }.compilesWithoutError()
- }
-
- @Test
- fun testCollate() {
- validQueryTest("SELECT id, name FROM user ORDER BY name COLLATE LOCALIZED ASC") {
- assertThat(it, `is`(
- QueryResultInfo(listOf(
- // unfortunately, we don't get this information
- ColumnInfo("id", SQLTypeAffinity.INTEGER),
- ColumnInfo("name", SQLTypeAffinity.TEXT)
- ))))
- }
- }
-
- @Test
- fun testCollateBasQuery() {
- simpleRun { invocation ->
- val verifier = createVerifier(invocation)
- val (_, error) = verifier.analyze(
- "SELECT id, name FROM user ORDER BY name COLLATE LOCALIZEDASC")
- assertThat(error, notNullValue())
- }.compilesWithoutError()
- }
-
- private fun validQueryTest(sql: String, cb: (QueryResultInfo) -> Unit) {
- simpleRun { invocation ->
- val verifier = createVerifier(invocation)
- val info = verifier.analyze(sql)
- cb(info)
- }.compilesWithoutError()
- }
-
- private fun userDb(context: Context): Database {
- return database(entity("User",
- field("id", primitive(context, TypeKind.INT), SQLTypeAffinity.INTEGER),
- field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
- field("lastName", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
- field("ratio", primitive(context, TypeKind.FLOAT), SQLTypeAffinity.REAL)))
- }
-
- private fun database(vararg entities: Entity): Database {
- return Database(
- element = mock(TypeElement::class.java),
- type = mock(TypeMirror::class.java),
- entities = entities.toList(),
- daoMethods = emptyList(),
- version = -1,
- exportSchema = false,
- enableForeignKeys = false)
- }
-
- private fun entity(tableName: String, vararg fields: Field): Entity {
- return Entity(
- element = mock(TypeElement::class.java),
- tableName = tableName,
- type = mock(DeclaredType::class.java),
- fields = fields.toList(),
- embeddedFields = emptyList(),
- indices = emptyList(),
- primaryKey = PrimaryKey(null, fields.take(1), false),
- foreignKeys = emptyList(),
- constructor = Constructor(mock(ExecutableElement::class.java), emptyList())
- )
- }
-
- private fun field(name: String, type: TypeMirror, affinity: SQLTypeAffinity): Field {
- val element = mock(Element::class.java)
- doReturn(type).`when`(element).asType()
- val f = Field(
- element = element,
- name = name,
- type = type,
- columnName = name,
- affinity = affinity,
- collate = if (useLocalizedCollation && affinity == SQLTypeAffinity.TEXT) {
- Collate.LOCALIZED
- } else {
- null
- }
- )
- assignGetterSetter(f, name, type)
- return f
- }
-
- private fun assignGetterSetter(f: Field, name: String, type: TypeMirror) {
- f.getter = FieldGetter(name, type, CallType.FIELD)
- f.setter = FieldSetter(name, type, CallType.FIELD)
- }
-
- private fun primitive(context: Context, kind: TypeKind): PrimitiveType {
- return context.processingEnv.typeUtils.getPrimitiveType(kind)
- }
-
- private fun getPrimaryKeys(connection: Connection, tableName: String): List<String> {
- val stmt = connection.createStatement()
- val resultSet = stmt.executeQuery("PRAGMA table_info($tableName)")
- return resultSet.collect {
- Pair(it.getString("name"), it.getInt("pk"))
- }
- .filter { it.second > 0 }
- .sortedBy { it.second }
- .map { it.first }
- }
-
- companion object {
- @Parameterized.Parameters(name = "useLocalizedCollation={0}")
- @JvmStatic
- fun params() = arrayListOf(true, false)
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/vo/EntityTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/vo/EntityTest.kt
deleted file mode 100644
index 8afa935..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/vo/EntityTest.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2018 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.vo
-
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
-import javax.lang.model.element.Element
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.DeclaredType
-
-@RunWith(JUnit4::class)
-class EntityTest {
-
- @Test
- fun shouldBeDeletedAfter() {
- val child = createEntity("Child", listOf(
- createForeignKey("NoAction", ForeignKeyAction.NO_ACTION, false),
- createForeignKey("NoActionDeferred", ForeignKeyAction.NO_ACTION, true),
- createForeignKey("Restrict", ForeignKeyAction.RESTRICT, false),
- createForeignKey("RestrictDeferred", ForeignKeyAction.RESTRICT, true),
- createForeignKey("SetNull", ForeignKeyAction.SET_NULL, false),
- createForeignKey("SetNullDeferred", ForeignKeyAction.SET_NULL, true),
- createForeignKey("SetDefault", ForeignKeyAction.SET_DEFAULT, false),
- createForeignKey("SetDefaultDeferred", ForeignKeyAction.SET_DEFAULT, true),
- createForeignKey("Cascade", ForeignKeyAction.CASCADE, false),
- createForeignKey("CascadeDeferred", ForeignKeyAction.CASCADE, true)))
- val noAction = createEntity("NoAction")
- val noActionDeferred = createEntity("NoActionDeferred")
- val restrict = createEntity("Restrict")
- val restrictDeferred = createEntity("RestrictDeferred")
- val setNull = createEntity("SetNull")
- val setNullDeferred = createEntity("SetNullDeferred")
- val setDefault = createEntity("SetDefault")
- val setDefaultDeferred = createEntity("SetDefaultDeferred")
- val cascade = createEntity("Cascade")
- val cascadeDeferred = createEntity("CascadeDeferred")
- val irrelevant = createEntity("Irrelevant")
- assertThat(child.shouldBeDeletedAfter(noAction), `is`(true))
- assertThat(child.shouldBeDeletedAfter(noActionDeferred), `is`(false))
- assertThat(child.shouldBeDeletedAfter(restrict), `is`(true))
- assertThat(child.shouldBeDeletedAfter(restrictDeferred), `is`(true))
- assertThat(child.shouldBeDeletedAfter(setNull), `is`(false))
- assertThat(child.shouldBeDeletedAfter(setNullDeferred), `is`(false))
- assertThat(child.shouldBeDeletedAfter(setDefault), `is`(false))
- assertThat(child.shouldBeDeletedAfter(setDefaultDeferred), `is`(false))
- assertThat(child.shouldBeDeletedAfter(cascade), `is`(false))
- assertThat(child.shouldBeDeletedAfter(cascadeDeferred), `is`(false))
- assertThat(child.shouldBeDeletedAfter(irrelevant), `is`(false))
- }
-
- private fun createEntity(
- tableName: String,
- foreignKeys: List<ForeignKey> = emptyList()): Entity {
- return Entity(
- element = mock(TypeElement::class.java),
- tableName = tableName,
- type = mock(DeclaredType::class.java),
- fields = emptyList(),
- embeddedFields = emptyList(),
- primaryKey = PrimaryKey(mock(Element::class.java), emptyList(), false),
- indices = emptyList(),
- foreignKeys = foreignKeys,
- constructor = Constructor(mock(ExecutableElement::class.java), emptyList()))
- }
-
- private fun createForeignKey(
- parentTable: String,
- onDelete: ForeignKeyAction,
- deferred: Boolean): ForeignKey {
- return ForeignKey(
- parentTable = parentTable,
- parentColumns = emptyList(),
- childFields = emptyList(),
- onDelete = onDelete,
- onUpdate = ForeignKeyAction.NO_ACTION,
- deferred = deferred)
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/vo/IndexTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/vo/IndexTest.kt
deleted file mode 100644
index 04e43f8..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/vo/IndexTest.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.vo
-
-import android.arch.persistence.room.parser.SQLTypeAffinity
-import mockElementAndType
-import org.hamcrest.CoreMatchers
-import org.hamcrest.MatcherAssert
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class IndexTest {
- @Test
- fun createSimpleSQL() {
- val index = Index("foo", false, listOf(mockField("bar"), mockField("baz")))
- MatcherAssert.assertThat(index.createQuery("my_table"), CoreMatchers.`is`(
- "CREATE INDEX `foo` ON `my_table` (`bar`, `baz`)"
- ))
- }
-
- @Test
- fun createUnique() {
- val index = Index("foo", true, listOf(mockField("bar"), mockField("baz")))
- MatcherAssert.assertThat(index.createQuery("my_table"), CoreMatchers.`is`(
- "CREATE UNIQUE INDEX `foo` ON `my_table` (`bar`, `baz`)"
- ))
- }
-
- private fun mockField(columnName: String): Field {
- val (element, type) = mockElementAndType()
- return Field(
- element = element,
- name = columnName + "_field",
- affinity = SQLTypeAffinity.TEXT,
- type = type,
- columnName = columnName
- )
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/DaoWriterTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/DaoWriterTest.kt
deleted file mode 100644
index 96d1e3a..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/DaoWriterTest.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import COMMON
-import android.arch.persistence.room.ext.RoomTypeNames
-import android.arch.persistence.room.processor.DaoProcessor
-import android.arch.persistence.room.testing.TestProcessor
-import com.google.auto.common.MoreElements
-import com.google.auto.common.MoreTypes
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import createVerifierFromEntities
-import loadJavaCode
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.tools.JavaFileObject
-
-@RunWith(JUnit4::class)
-class DaoWriterTest {
- @Test
- fun complexDao() {
- singleDao(
- loadJavaCode("databasewriter/input/ComplexDatabase.java",
- "foo.bar.ComplexDatabase"),
- loadJavaCode("daoWriter/input/ComplexDao.java", "foo.bar.ComplexDao")
- ).compilesWithoutError().and().generatesSources(
- loadJavaCode("daoWriter/output/ComplexDao.java", "foo.bar.ComplexDao_Impl")
- )
- }
-
- @Test
- fun writerDao() {
- singleDao(
- loadJavaCode("daoWriter/input/WriterDao.java", "foo.bar.WriterDao")
- ).compilesWithoutError().and().generatesSources(
- loadJavaCode("daoWriter/output/WriterDao.java", "foo.bar.WriterDao_Impl")
- )
- }
-
- @Test
- fun deletionDao() {
- singleDao(
- loadJavaCode("daoWriter/input/DeletionDao.java", "foo.bar.DeletionDao")
- ).compilesWithoutError().and().generatesSources(
- loadJavaCode("daoWriter/output/DeletionDao.java", "foo.bar.DeletionDao_Impl")
- )
- }
-
- @Test
- fun updateDao() {
- singleDao(
- loadJavaCode("daoWriter/input/UpdateDao.java", "foo.bar.UpdateDao")
- ).compilesWithoutError().and().generatesSources(
- loadJavaCode("daoWriter/output/UpdateDao.java", "foo.bar.UpdateDao_Impl")
- )
- }
-
- fun singleDao(vararg jfo: JavaFileObject): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfo.toList() + COMMON.USER + COMMON.MULTI_PKEY_ENTITY + COMMON.BOOK +
- COMMON.LIVE_DATA + COMMON.COMPUTABLE_LIVE_DATA)
- .processedWith(TestProcessor.builder()
- .forAnnotations(android.arch.persistence.room.Dao::class)
- .nextRunHandler { invocation ->
- val dao = invocation.roundEnv
- .getElementsAnnotatedWith(
- android.arch.persistence.room.Dao::class.java)
- .first()
- val db = invocation.roundEnv
- .getElementsAnnotatedWith(
- android.arch.persistence.room.Database::class.java)
- .firstOrNull()
- val dbType = MoreTypes.asDeclared(if (db != null) {
- db.asType()
- } else {
- invocation.context.processingEnv.elementUtils
- .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType()
- })
- val parser = DaoProcessor(
- baseContext = invocation.context,
- element = MoreElements.asType(dao),
- dbType = dbType,
- dbVerifier = createVerifierFromEntities(invocation))
- val parsedDao = parser.process()
- DaoWriter(parsedDao, invocation.processingEnv)
- .write(invocation.processingEnv)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/DatabaseWriterTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/DatabaseWriterTest.kt
deleted file mode 100644
index 824d2d0..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/DatabaseWriterTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import COMMON
-import android.arch.persistence.room.RoomProcessor
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import loadJavaCode
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.tools.JavaFileObject
-
-@RunWith(JUnit4::class)
-class DatabaseWriterTest {
- @Test
- fun simpleDb() {
- singleDb(
- loadJavaCode("databasewriter/input/ComplexDatabase.java",
- "foo.bar.ComplexDatabase"),
- loadJavaCode("daoWriter/input/ComplexDao.java",
- "foo.bar.ComplexDao")
- ).compilesWithoutError().and().generatesSources(
- loadJavaCode("databasewriter/output/ComplexDatabase.java",
- "foo.bar.ComplexDatabase_Impl")
- )
- }
-
- private fun singleDb(vararg jfo: JavaFileObject): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfo.toList() + COMMON.USER + COMMON.LIVE_DATA + COMMON.COMPUTABLE_LIVE_DATA)
- .processedWith(RoomProcessor())
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriterTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriterTest.kt
deleted file mode 100644
index 0376ec0..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/EntityCursorConverterWriterTest.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.processor.BaseEntityParserTest
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.TypeSpec
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import javax.lang.model.element.Modifier
-
-@RunWith(JUnit4::class)
-class EntityCursorConverterWriterTest : BaseEntityParserTest() {
- companion object {
- val OUT_PREFIX = """
- package foo.bar;
- import android.database.Cursor;
- import java.lang.SuppressWarnings;
- import javax.annotation.Generated;
- @Generated("android.arch.persistence.room.RoomProcessor")
- @SuppressWarnings("unchecked")
- public class MyContainerClass {
- """.trimIndent()
- const val OUT_SUFFIX = "}"
- }
-
- @Test
- fun generateSimple() {
- generateAndMatch(
- """
- @PrimaryKey
- private int id;
- String name;
- String lastName;
- int age;
- public int getId() { return id; }
- public void setId(int id) { this.id = id; }
- """,
- """
- private MyEntity __entityCursorConverter_fooBarMyEntity(Cursor cursor) {
- final MyEntity _entity;
- final int _cursorIndexOfId = cursor.getColumnIndex("id");
- final int _cursorIndexOfName = cursor.getColumnIndex("name");
- final int _cursorIndexOfLastName = cursor.getColumnIndex("lastName");
- final int _cursorIndexOfAge = cursor.getColumnIndex("age");
- _entity = new MyEntity();
- if (_cursorIndexOfId != -1) {
- final int _tmpId;
- _tmpId = cursor.getInt(_cursorIndexOfId);
- _entity.setId(_tmpId);
- }
- if (_cursorIndexOfName != -1) {
- _entity.name = cursor.getString(_cursorIndexOfName);
- }
- if (_cursorIndexOfLastName != -1) {
- _entity.lastName = cursor.getString(_cursorIndexOfLastName);
- }
- if (_cursorIndexOfAge != -1) {
- _entity.age = cursor.getInt(_cursorIndexOfAge);
- }
- return _entity;
- }
- """.trimIndent())
- }
-
- fun generateAndMatch(input: String, output: String,
- attributes: Map<String, String> = mapOf()) {
- generate(input, attributes)
- .compilesWithoutError()
- .and()
- .generatesSources(JavaFileObjects.forSourceString(
- "foo.bar.MyEntity_CursorConverter",
- listOf(OUT_PREFIX, output, OUT_SUFFIX).joinToString("\n")))
- }
-
- fun generate(input: String, attributes: Map<String, String> = mapOf()): CompileTester {
- return singleEntity(input, attributes) { entity, invocation ->
- val className = ClassName.get("foo.bar", "MyContainerClass")
- val writer = object : ClassWriter(className) {
- override fun createTypeSpecBuilder(): TypeSpec.Builder {
- getOrCreateMethod(EntityCursorConverterWriter(entity))
- return TypeSpec.classBuilder(className).apply {
- addModifiers(Modifier.PUBLIC)
- }
- }
- }
- writer.write(invocation.processingEnv)
- }
- }
-}
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriterTest.kt
deleted file mode 100644
index 4d68231..0000000
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/writer/SQLiteOpenHelperWriterTest.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2016 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.writer
-
-import android.arch.persistence.room.processor.DatabaseProcessor
-import android.arch.persistence.room.testing.TestInvocation
-import android.arch.persistence.room.testing.TestProcessor
-import android.arch.persistence.room.vo.Database
-import android.support.annotation.NonNull
-import com.google.auto.common.MoreElements
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class SQLiteOpenHelperWriterTest {
- companion object {
- const val ENTITY_PREFIX = """
- package foo.bar;
- import android.support.annotation.NonNull;
- import android.arch.persistence.room.*;
- @Entity%s
- public class MyEntity {
- """
- const val ENTITY_SUFFIX = "}"
- const val DATABASE_CODE = """
- package foo.bar;
- import android.arch.persistence.room.*;
- @Database(entities = {MyEntity.class}, version = 3)
- abstract public class MyDatabase extends RoomDatabase {
- }
- """
- }
-
- @Test
- fun createSimpleEntity() {
- singleEntity(
- """
- @PrimaryKey
- @NonNull
- String uuid;
- String name;
- int age;
- """.trimIndent()
- ) { database, _ ->
- val query = SQLiteOpenHelperWriter(database)
- .createQuery(database.entities.first())
- assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
- " `MyEntity` (`uuid` TEXT NOT NULL, `name` TEXT, `age` INTEGER NOT NULL," +
- " PRIMARY KEY(`uuid`))"))
- }.compilesWithoutError()
- }
-
- @Test
- fun multiplePrimaryKeys() {
- singleEntity(
- """
- @NonNull
- String uuid;
- @NonNull
- String name;
- int age;
- """.trimIndent(), attributes = mapOf("primaryKeys" to "{\"uuid\", \"name\"}")
- ) { database, _ ->
- val query = SQLiteOpenHelperWriter(database)
- .createQuery(database.entities.first())
- assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
- " `MyEntity` (`uuid` TEXT NOT NULL, `name` TEXT NOT NULL, " +
- "`age` INTEGER NOT NULL, PRIMARY KEY(`uuid`, `name`))"))
- }.compilesWithoutError()
- }
-
- @Test
- fun autoIncrementObject() {
- listOf("Long", "Integer").forEach { type ->
- singleEntity(
- """
- @PrimaryKey(autoGenerate = true)
- $type uuid;
- String name;
- int age;
- """.trimIndent()
- ) { database, _ ->
- val query = SQLiteOpenHelperWriter(database)
- .createQuery(database.entities.first())
- assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
- " `MyEntity` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT," +
- " `name` TEXT, `age` INTEGER NOT NULL)"))
- }.compilesWithoutError()
- }
- }
-
- @Test
- fun autoIncrementPrimitives() {
- listOf("long", "int").forEach { type ->
- singleEntity(
- """
- @PrimaryKey(autoGenerate = true)
- $type uuid;
- String name;
- int age;
- """.trimIndent()
- ) { database, _ ->
- val query = SQLiteOpenHelperWriter(database)
- .createQuery(database.entities.first())
- assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
- " `MyEntity` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
- " `name` TEXT, `age` INTEGER NOT NULL)"))
- }.compilesWithoutError()
- }
- }
-
- fun singleEntity(input: String, attributes: Map<String, String> = mapOf(),
- handler: (Database, TestInvocation) -> Unit): CompileTester {
- val attributesReplacement: String
- if (attributes.isEmpty()) {
- attributesReplacement = ""
- } else {
- attributesReplacement = "(" +
- attributes.entries.map { "${it.key} = ${it.value}" }.joinToString(",") +
- ")".trimIndent()
- }
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
- ENTITY_PREFIX.format(attributesReplacement) + input + ENTITY_SUFFIX
- ), JavaFileObjects.forSourceString("foo.bar.MyDatabase",
- DATABASE_CODE)))
- .processedWith(TestProcessor.builder()
- .forAnnotations(android.arch.persistence.room.Database::class,
- NonNull::class)
- .nextRunHandler { invocation ->
- val db = MoreElements.asType(invocation.roundEnv
- .getElementsAnnotatedWith(
- android.arch.persistence.room.Database::class.java)
- .first())
- handler(DatabaseProcessor(invocation.context, db).process(), invocation)
- true
- }
- .build())
- }
-}
diff --git a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
new file mode 100644
index 0000000..0dff752
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.parser
+
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class SqlParserTest {
+
+ @Test
+ fun multipleQueries() {
+ assertErrors("SELECT * FROM users; SELECT * FROM books;",
+ ParserErrors.NOT_ONE_QUERY)
+ }
+
+ @Test
+ fun empty() {
+ assertErrors("", ParserErrors.NOT_ONE_QUERY)
+ }
+
+ @Test
+ fun deleteQuery() {
+ val parsed = SqlParser.parse("DELETE FROM users where id > 3")
+ assertThat(parsed.errors, `is`(emptyList()))
+ assertThat(parsed.type, `is`(QueryType.DELETE))
+ }
+
+ @Test
+ fun badDeleteQuery() {
+ assertErrors("delete from user where mAge >= :min && mAge <= :max",
+ "no viable alternative at input 'delete from user where mAge >= :min &&'")
+ }
+
+ @Test
+ fun updateQuery() {
+ val parsed = SqlParser.parse("UPDATE users set name = :name where id = :id")
+ assertThat(parsed.errors, `is`(emptyList()))
+ assertThat(parsed.type, `is`(QueryType.UPDATE))
+ }
+
+ @Test
+ fun explain() {
+ assertErrors("EXPLAIN QUERY PLAN SELECT * FROM users",
+ ParserErrors.invalidQueryType(QueryType.EXPLAIN))
+ }
+
+ @Test
+ fun validColumnNames() {
+ listOf("f", "fo", "f2", "f 2", "foo_2", "foo-2", "_", "foo bar baz",
+ "foo 2 baz", "_baz", "fooBar", "2", "*", "foo*2", "dsa$", "\$fsa",
+ "-bar", "ÅŸoöÄŸüı").forEach {
+ assertThat("name: $it", SqlParser.isValidIdentifier(it), `is`(true))
+ }
+ }
+
+ @Test
+ fun invalidColumnNames() {
+ listOf("", " ", "fd`a`", "f`a", "`a", "\"foo bar\"", "\"", "`").forEach {
+ assertThat("name: $it", SqlParser.isValidIdentifier(it), `is`(false))
+ }
+ }
+
+ @Test
+ fun extractTableNames() {
+ assertThat(SqlParser.parse("select * from users").tables,
+ `is`(setOf(Table("users", "users"))))
+ assertThat(SqlParser.parse("select * from users as ux").tables,
+ `is`(setOf(Table("users", "ux"))))
+ assertThat(SqlParser.parse("select * from (select * from books)").tables,
+ `is`(setOf(Table("books", "books"))))
+ assertThat(SqlParser.parse("select x.id from (select * from books) as x").tables,
+ `is`(setOf(Table("books", "books"))))
+ }
+
+ @Test
+ fun unescapeTableNames() {
+ assertThat(SqlParser.parse("select * from `users`").tables,
+ `is`(setOf(Table("users", "users"))))
+ assertThat(SqlParser.parse("select * from \"users\"").tables,
+ `is`(setOf(Table("users", "users"))))
+ assertThat(SqlParser.parse("select * from 'users'").tables,
+ `is`(setOf(Table("users", "users"))))
+ }
+
+ @Test
+ fun tablePrefixInInsert_set() {
+ // this is an invalid query, b/64539805
+ val query = SqlParser.parse("UPDATE trips SET trips.title=:title")
+ assertThat(query.errors, not(emptyList()))
+ }
+
+ @Test
+ fun tablePrefixInInsert_where() {
+ val query = SqlParser.parse("UPDATE trips SET title=:title WHERE trips.id=:id")
+ assertThat(query.errors, `is`(emptyList()))
+ }
+
+ @Test
+ fun tablePrefixInSelect_projection() {
+ val query = SqlParser.parse("SELECT a.name, b.last_name from user a, book b")
+ assertThat(query.errors, `is`(emptyList()))
+ assertThat(query.tables, `is`(setOf(Table("user", "a"),
+ Table("book", "b"))))
+ }
+
+ @Test
+ fun tablePrefixInSelect_where() {
+ val query = SqlParser.parse("SELECT a.name, b.last_name from user a, book b" +
+ " WHERE a.name = b.name")
+ assertThat(query.errors, `is`(emptyList()))
+ assertThat(query.tables, `is`(setOf(Table("user", "a"),
+ Table("book", "b"))))
+ }
+
+ @Test
+ fun findBindVariables() {
+ assertVariables("select * from users")
+ assertVariables("select * from users where name like ?", "?")
+ assertVariables("select * from users where name like :name", ":name")
+ assertVariables("select * from users where name like ?2", "?2")
+ assertVariables("select * from users where name like ?2 OR name LIKE ?1", "?2", "?1")
+ assertVariables("select * from users where name like @a", "@a")
+ assertVariables("select * from users where name like \$a", "\$a")
+ }
+
+ @Test
+ fun indexedVariablesError() {
+ assertErrors("select * from users where name like ?",
+ ParserErrors.ANONYMOUS_BIND_ARGUMENT)
+ assertErrors("select * from users where name like ? or last_name like ?",
+ ParserErrors.ANONYMOUS_BIND_ARGUMENT)
+ assertErrors("select * from users where name like ?1",
+ ParserErrors.cannotUseVariableIndices("?1", 36))
+ }
+
+ @Test
+ fun foo() {
+ assertSections("select * from users where name like ?",
+ Section.text("select * from users where name like "),
+ Section.bindVar("?"))
+
+ assertSections("select * from users where name like :name AND last_name like :lastName",
+ Section.text("select * from users where name like "),
+ Section.bindVar(":name"),
+ Section.text(" AND last_name like "),
+ Section.bindVar(":lastName"))
+
+ assertSections("select * from users where name \nlike :name AND last_name like :lastName",
+ Section.text("select * from users where name "),
+ Section.newline(),
+ Section.text("like "),
+ Section.bindVar(":name"),
+ Section.text(" AND last_name like "),
+ Section.bindVar(":lastName"))
+
+ assertSections("select * from users where name like :name \nAND last_name like :lastName",
+ Section.text("select * from users where name like "),
+ Section.bindVar(":name"),
+ Section.text(" "),
+ Section.newline(),
+ Section.text("AND last_name like "),
+ Section.bindVar(":lastName"))
+
+ assertSections("select * from users where name like :name \nAND last_name like \n:lastName",
+ Section.text("select * from users where name like "),
+ Section.bindVar(":name"),
+ Section.text(" "),
+ Section.newline(),
+ Section.text("AND last_name like "),
+ Section.newline(),
+ Section.bindVar(":lastName"))
+ }
+
+ fun assertVariables(query: String, vararg expected: String) {
+ assertThat((SqlParser.parse(query)).inputs.map { it.text }, `is`(expected.toList()))
+ }
+
+ fun assertErrors(query: String, vararg errors: String) {
+ assertThat((SqlParser.parse(query)).errors, `is`(errors.toList()))
+ }
+
+ fun assertSections(query: String, vararg sections: Section) {
+ assertThat(SqlParser.parse(query).sections, `is`(sections.toList()))
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
new file mode 100644
index 0000000..b3d42b7
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
@@ -0,0 +1,169 @@
+package androidx.room.processor
+
+import COMMON
+import androidx.room.ext.RoomTypeNames
+import androidx.room.vo.Dao
+import androidx.room.writer.DaoWriter
+import com.google.auto.common.MoreTypes
+import com.google.testing.compile.JavaFileObjects
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import simpleRun
+
+/**
+ * we don't assert much in these tests since if type resolution fails, compilation fails.
+ */
+@RunWith(JUnit4::class)
+class BaseDaoTest {
+ private fun String.toJFO(qName: String) = JavaFileObjects.forSourceLines(qName, this)
+
+ @Test
+ fun insert() {
+ baseDao("""
+ @Insert
+ void insertMe(T t);
+ """) { dao ->
+ assertThat(dao.insertionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun insertArray() {
+ baseDao("""
+ @Insert
+ void insertMe(T[] t);
+ """) { dao ->
+ assertThat(dao.insertionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun insertVarArg() {
+ baseDao("""
+ @Insert
+ void insertMe(T... t);
+ """) { dao ->
+ assertThat(dao.insertionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun insertList() {
+ baseDao("""
+ @Insert
+ void insertMe(List<T> t);
+ """) { dao ->
+ assertThat(dao.insertionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun delete() {
+ baseDao("""
+ @Delete
+ void deleteMe(T t);
+ """) { dao ->
+ assertThat(dao.deletionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun deleteArray() {
+ baseDao("""
+ @Delete
+ void deleteMe(T[] t);
+ """) { dao ->
+ assertThat(dao.deletionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun deleteVarArg() {
+ baseDao("""
+ @Delete
+ void deleteMe(T... t);
+ """) { dao ->
+ assertThat(dao.deletionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun deleteList() {
+ baseDao("""
+ @Delete
+ void deleteMe(List<T> t);
+ """) { dao ->
+ assertThat(dao.deletionMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun update() {
+ baseDao("""
+ @Update
+ void updateMe(T t);
+ """) { dao ->
+ assertThat(dao.updateMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun updateArray() {
+ baseDao("""
+ @Update
+ void updateMe(T[] t);
+ """) { dao ->
+ assertThat(dao.updateMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun updateVarArg() {
+ baseDao("""
+ @Update
+ void updateMe(T... t);
+ """) { dao ->
+ assertThat(dao.updateMethods.size, `is`(1))
+ }
+ }
+
+ @Test
+ fun updateList() {
+ baseDao("""
+ @Update
+ void updateMe(List<T> t);
+ """) { dao ->
+ assertThat(dao.updateMethods.size, `is`(1))
+ }
+ }
+
+ fun baseDao(code: String, handler: (Dao) -> Unit) {
+ val baseClass = """
+ package foo.bar;
+ import androidx.room.*;
+ import java.util.List;
+
+ interface BaseDao<K, T> {
+ $code
+ }
+ """.toJFO("foo.bar.BaseDao")
+ val extension = """
+ package foo.bar;
+ import androidx.room.*;
+ @Dao
+ interface MyDao extends BaseDao<Integer, User> {
+ }
+ """.toJFO("foo.bar.MyDao")
+ simpleRun(baseClass, extension, COMMON.USER) { invocation ->
+ val daoElm = invocation.processingEnv.elementUtils.getTypeElement("foo.bar.MyDao")
+ val dbType = MoreTypes.asDeclared(invocation.context.processingEnv.elementUtils
+ .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType())
+ val processedDao = DaoProcessor(invocation.context, daoElm, dbType, null).process()
+ handler(processedDao)
+ DaoWriter(processedDao, invocation.processingEnv).write(invocation.processingEnv)
+ }.compilesWithoutError()
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
new file mode 100644
index 0000000..0bd4b2f
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.annotation.NonNull
+import androidx.room.Embedded
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.Entity
+import com.google.auto.common.MoreElements
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import javax.tools.JavaFileObject
+
+abstract class BaseEntityParserTest {
+ companion object {
+ const val ENTITY_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ import androidx.annotation.NonNull;
+ @Entity%s
+ public class MyEntity %s {
+ """
+ const val ENTITY_SUFFIX = "}"
+ }
+
+ fun singleEntity(input: String, attributes: Map<String, String> = mapOf(),
+ baseClass: String = "",
+ jfos: List<JavaFileObject> = emptyList(),
+ handler: (Entity, TestInvocation) -> Unit): CompileTester {
+ val attributesReplacement: String
+ if (attributes.isEmpty()) {
+ attributesReplacement = ""
+ } else {
+ attributesReplacement = "(" +
+ attributes.entries.map { "${it.key} = ${it.value}" }.joinToString(",") +
+ ")".trimIndent()
+ }
+ val baseClassReplacement: String
+ if (baseClass == "") {
+ baseClassReplacement = ""
+ } else {
+ baseClassReplacement = " extends $baseClass"
+ }
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(jfos + JavaFileObjects.forSourceString("foo.bar.MyEntity",
+ ENTITY_PREFIX.format(attributesReplacement, baseClassReplacement)
+ + input + ENTITY_SUFFIX
+ ))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(androidx.room.Entity::class,
+ androidx.room.PrimaryKey::class,
+ androidx.room.Ignore::class,
+ Embedded::class,
+ androidx.room.ColumnInfo::class,
+ NonNull::class)
+ .nextRunHandler { invocation ->
+ val entity = invocation.roundEnv
+ .getElementsAnnotatedWith(
+ androidx.room.Entity::class.java)
+ .first { it.toString() == "foo.bar.MyEntity" }
+ val parser = EntityProcessor(invocation.context,
+ MoreElements.asType(entity))
+ val parsedQuery = parser.process()
+ handler(parsedQuery, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
new file mode 100644
index 0000000..40d56e1
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/CustomConverterProcessorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.TypeConverter
+import androidx.room.ext.typeName
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
+import androidx.room.processor.ProcessorErrors
+ .TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MUST_BE_PUBLIC
+import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_UNBOUND_GENERIC
+import androidx.room.testing.TestInvocation
+import androidx.room.vo.CustomTypeConverter
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import com.squareup.javapoet.TypeVariableName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import simpleRun
+import java.util.Date
+import javax.lang.model.element.Modifier
+import javax.tools.JavaFileObject
+
+@RunWith(JUnit4::class)
+class CustomConverterProcessorTest {
+ companion object {
+ val CONVERTER = ClassName.get("foo.bar", "MyConverter")!!
+ val CONVERTER_QNAME = CONVERTER.packageName() + "." + CONVERTER.simpleName()
+ val CONTAINER = JavaFileObjects.forSourceString("foo.bar.Container",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @TypeConverters(foo.bar.MyConverter.class)
+ public class Container {}
+ """)
+ }
+
+ @Test
+ fun validCase() {
+ singleClass(createConverter(TypeName.SHORT.box(), TypeName.CHAR.box())) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.SHORT.box()))
+ assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primitiveFrom() {
+ singleClass(createConverter(TypeName.SHORT, TypeName.CHAR.box())) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
+ assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primitiveTo() {
+ singleClass(createConverter(TypeName.INT.box(), TypeName.DOUBLE)) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.INT.box()))
+ assertThat(converter?.toTypeName, `is`(TypeName.DOUBLE))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primitiveBoth() {
+ singleClass(createConverter(TypeName.INT, TypeName.DOUBLE)) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.INT))
+ assertThat(converter?.toTypeName, `is`(TypeName.DOUBLE))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun nonNullButNotBoxed() {
+ val string = String::class.typeName()
+ val date = Date::class.typeName()
+ singleClass(createConverter(string, date)) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(string as TypeName))
+ assertThat(converter?.toTypeName, `is`(date as TypeName))
+ }
+ }
+
+ @Test
+ fun parametrizedTypeUnbound() {
+ val typeVarT = TypeVariableName.get("T")
+ val list = ParameterizedTypeName.get(List::class.typeName(), typeVarT)
+ val typeVarK = TypeVariableName.get("K")
+ val map = ParameterizedTypeName.get(Map::class.typeName(), typeVarK, typeVarT)
+ singleClass(createConverter(list, map, listOf(typeVarK, typeVarT))) {
+ _, _ ->
+ }.failsToCompile().withErrorContaining(TYPE_CONVERTER_UNBOUND_GENERIC)
+ }
+
+ @Test
+ fun parametrizedTypeSpecific() {
+ val string = String::class.typeName()
+ val date = Date::class.typeName()
+ val list = ParameterizedTypeName.get(List::class.typeName(), string)
+ val map = ParameterizedTypeName.get(Map::class.typeName(), string, date)
+ singleClass(createConverter(list, map)) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(list as TypeName))
+ assertThat(converter?.toTypeName, `is`(map as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testNoConverters() {
+ singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
+ """
+ package ${CONVERTER.packageName()};
+ public class ${CONVERTER.simpleName()} {
+ }
+ """)) { _, _ ->
+ }.failsToCompile().withErrorContaining(TYPE_CONVERTER_EMPTY_CLASS)
+ }
+
+ @Test
+ fun checkNoArgConstructor() {
+ singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
+ """
+ package ${CONVERTER.packageName()};
+ import androidx.room.TypeConverter;
+
+ public class ${CONVERTER.simpleName()} {
+ public ${CONVERTER.simpleName()}(int x) {}
+ @TypeConverter
+ public int x(short y) {return 0;}
+ }
+ """)) { _, _ ->
+ }.failsToCompile().withErrorContaining(TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
+ }
+
+ @Test
+ fun checkNoArgConstructor_withStatic() {
+ singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
+ """
+ package ${CONVERTER.packageName()};
+ import androidx.room.TypeConverter;
+
+ public class ${CONVERTER.simpleName()} {
+ public ${CONVERTER.simpleName()}(int x) {}
+ @TypeConverter
+ public static int x(short y) {return 0;}
+ }
+ """)) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
+ assertThat(converter?.toTypeName, `is`(TypeName.INT))
+ assertThat(converter?.isStatic, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun checkPublic() {
+ singleClass(JavaFileObjects.forSourceString(CONVERTER_QNAME,
+ """
+ package ${CONVERTER.packageName()};
+ import androidx.room.TypeConverter;
+
+ public class ${CONVERTER.simpleName()} {
+ @TypeConverter static int x(short y) {return 0;}
+ @TypeConverter private static int y(boolean y) {return 0;}
+ }
+ """)) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.SHORT))
+ assertThat(converter?.toTypeName, `is`(TypeName.INT))
+ assertThat(converter?.isStatic, `is`(true))
+ }.failsToCompile().withErrorContaining(TYPE_CONVERTER_MUST_BE_PUBLIC).and()
+ .withErrorCount(2)
+ }
+
+ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ @Test
+ fun parametrizedTypeBoundViaParent() {
+ val typeVarT = TypeVariableName.get("T")
+ val list = ParameterizedTypeName.get(List::class.typeName(), typeVarT)
+ val typeVarK = TypeVariableName.get("K")
+ val map = ParameterizedTypeName.get(Map::class.typeName(), typeVarK, typeVarT)
+
+ val baseConverter = createConverter(list, map, listOf(typeVarT, typeVarK))
+ val extendingQName = "foo.bar.Extending"
+ val extendingClass = JavaFileObjects.forSourceString(extendingQName,
+ "package foo.bar;\n" +
+ TypeSpec.classBuilder(ClassName.bestGuess(extendingQName)).apply {
+ superclass(
+ ParameterizedTypeName.get(CONVERTER, String::class.typeName(),
+ Integer::class.typeName()))
+ }.build().toString())
+
+ simpleRun(baseConverter, extendingClass) { invocation ->
+ val element = invocation.processingEnv.elementUtils.getTypeElement(extendingQName)
+ val converter = CustomConverterProcessor(invocation.context, element)
+ .process().firstOrNull()
+ assertThat(converter?.fromTypeName, `is`(ParameterizedTypeName.get(
+ List::class.typeName(), String::class.typeName()) as TypeName
+ ))
+ assertThat(converter?.toTypeName, `is`(ParameterizedTypeName.get(Map::class.typeName(),
+ Integer::class.typeName(), String::class.typeName()) as TypeName
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun checkDuplicates() {
+ singleClass(
+ createConverter(TypeName.SHORT.box(), TypeName.CHAR.box(), duplicate = true)
+ ) { converter, _ ->
+ assertThat(converter?.fromTypeName, `is`(TypeName.SHORT.box()))
+ assertThat(converter?.toTypeName, `is`(TypeName.CHAR.box()))
+ }.failsToCompile().withErrorContaining("Multiple methods define the same conversion")
+ }
+
+ private fun createConverter(
+ from: TypeName,
+ to: TypeName,
+ typeVariables: List<TypeVariableName> = emptyList(),
+ duplicate: Boolean = false
+ ): JavaFileObject {
+ val code = TypeSpec.classBuilder(CONVERTER).apply {
+ addTypeVariables(typeVariables)
+ addModifiers(Modifier.PUBLIC)
+ fun buildMethod(name: String) = MethodSpec.methodBuilder(name).apply {
+ addAnnotation(TypeConverter::class.java)
+ addModifiers(Modifier.PUBLIC)
+ returns(to)
+ addParameter(ParameterSpec.builder(from, "input").build())
+ if (to.isPrimitive) {
+ addStatement("return 0")
+ } else {
+ addStatement("return null")
+ }
+ }.build()
+ addMethod(buildMethod("convertF"))
+ if (duplicate) {
+ addMethod(buildMethod("convertF2"))
+ }
+ }.build().toString()
+ return JavaFileObjects.forSourceString(CONVERTER.toString(),
+ "package ${CONVERTER.packageName()};\n$code")
+ }
+
+ private fun singleClass(
+ vararg jfo: JavaFileObject,
+ handler: (CustomTypeConverter?, TestInvocation) -> Unit
+ ): CompileTester {
+ return simpleRun(*((jfo.toList() + CONTAINER).toTypedArray())) { invocation ->
+ val processed = CustomConverterProcessor.findConverters(invocation.context,
+ invocation.processingEnv.elementUtils.getTypeElement("foo.bar.Container"))
+ handler(processed.converters.firstOrNull()?.custom, invocation)
+ }
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
new file mode 100644
index 0000000..4842451
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import COMMON
+import androidx.room.ext.RoomTypeNames
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.Dao
+import androidx.room.vo.Warning
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import createVerifierFromEntities
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+class DaoProcessorTest(val enableVerification: Boolean) {
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ """
+ @Parameterized.Parameters(name = "enableDbVerification={0}")
+ @JvmStatic
+ fun getParams() = arrayOf(true, false)
+ }
+
+ @Test
+ fun testNonAbstract() {
+ singleDao("@Dao public class MyDao {}") { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.DAO_MUST_BE_AN_ABSTRACT_CLASS_OR_AN_INTERFACE)
+ }
+
+ @Test
+ fun testAbstractMethodWithoutQuery() {
+ singleDao("""
+ @Dao public interface MyDao {
+ int getFoo();
+ }
+ """) { _, _ ->
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.ABSTRACT_METHOD_IN_DAO_MISSING_ANY_ANNOTATION)
+ }
+
+ @Test
+ fun testBothAnnotations() {
+ singleDao("""
+ @Dao public interface MyDao {
+ @Query("select 1")
+ @Insert
+ int getFoo(int x);
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.INVALID_ANNOTATION_COUNT_IN_DAO_METHOD)
+ }
+
+ @Test
+ fun testAbstractClass() {
+ singleDao("""
+ @Dao abstract class MyDao {
+ @Query("SELECT uid FROM User")
+ abstract int[] getIds();
+ }
+ """) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ val method = dao.queryMethods.first()
+ assertThat(method.name, `is`("getIds"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testInterface() {
+ singleDao("""
+ @Dao interface MyDao {
+ @Query("SELECT uid FROM User")
+ abstract int[] getIds();
+ }
+ """) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ val method = dao.queryMethods.first()
+ assertThat(method.name, `is`("getIds"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testWithInsertAndQuery() {
+ singleDao("""
+ @Dao abstract class MyDao {
+ @Query("SELECT uid FROM User")
+ abstract int[] getIds();
+ @Insert
+ abstract void insert(User user);
+ }
+ """) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ val method = dao.queryMethods.first()
+ assertThat(method.name, `is`("getIds"))
+ assertThat(dao.insertionMethods.size, `is`(1))
+ val insertMethod = dao.insertionMethods.first()
+ assertThat(insertMethod.name, `is`("insert"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun skipQueryVerification() {
+ singleDao("""
+ @Dao @SkipQueryVerification interface MyDao {
+ @Query("SELECT nonExistingField FROM User")
+ abstract int[] getIds();
+ }
+ """) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ val method = dao.queryMethods.first()
+ assertThat(method.name, `is`("getIds"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun suppressedWarnings() {
+ singleDao("""
+ @SuppressWarnings({"ALL", RoomWarnings.CURSOR_MISMATCH})
+ @Dao interface MyDao {
+ @Query("SELECT * from user")
+ abstract User users();
+ }
+ """) { dao, invocation ->
+ val dbType = MoreTypes.asDeclared(invocation.context.processingEnv.elementUtils
+ .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType())
+ val daoProcessor = DaoProcessor(invocation.context, dao.element, dbType, null)
+ assertThat(daoProcessor.context.logger
+ .suppressedWarnings, `is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH)))
+
+ dao.queryMethods.forEach {
+ assertThat(QueryMethodProcessor(
+ baseContext = daoProcessor.context,
+ containing = MoreTypes.asDeclared(dao.element.asType()),
+ executableElement = it.element,
+ dbVerifier = null).context.logger.suppressedWarnings,
+ `is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH)))
+ }
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun suppressedWarningsInheritance() {
+ singleDao("""
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Dao interface MyDao {
+ @SuppressWarnings("ALL")
+ @Query("SELECT * from user")
+ abstract User users();
+ }
+ """) { dao, invocation ->
+ val dbType = MoreTypes.asDeclared(invocation.context.processingEnv.elementUtils
+ .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType())
+ val daoProcessor = DaoProcessor(invocation.context, dao.element, dbType, null)
+ assertThat(daoProcessor.context.logger
+ .suppressedWarnings, `is`(setOf(Warning.CURSOR_MISMATCH)))
+
+ dao.queryMethods.forEach {
+ assertThat(QueryMethodProcessor(
+ baseContext = daoProcessor.context,
+ containing = MoreTypes.asDeclared(dao.element.asType()),
+ executableElement = it.element,
+ dbVerifier = null).context.logger.suppressedWarnings,
+ `is`(setOf(Warning.ALL, Warning.CURSOR_MISMATCH)))
+ }
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_warnIfTransactionIsMissingForRelation() {
+ if (!enableVerification) {
+ return
+ }
+ singleDao(
+ """
+ @Dao interface MyDao {
+ static class Merged extends User {
+ @Relation(parentColumn = "name", entityColumn = "lastName",
+ entity = User.class)
+ java.util.List<User> users;
+ }
+ @Query("select * from user")
+ abstract java.util.List<Merged> loadUsers();
+ }
+ """
+ ) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+ }.compilesWithoutError()
+ .withWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+ }
+
+ @Test
+ fun query_dontWarnIfTransactionIsMissingForRelation_suppressed() {
+ if (!enableVerification) {
+ return
+ }
+ singleDao(
+ """
+ @Dao interface MyDao {
+ static class Merged extends User {
+ @Relation(parentColumn = "name", entityColumn = "lastName",
+ entity = User.class)
+ java.util.List<User> users;
+ }
+ @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+ @Query("select * from user")
+ abstract java.util.List<Merged> loadUsers();
+ }
+ """
+ ) { dao, _ ->
+ assertThat(dao.queryMethods.size, `is`(1))
+ assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+ }.compilesWithoutError()
+ .withWarningCount(0)
+ }
+
+ @Test
+ fun query_dontWarnIfTransactionNotIsMissingForRelation() {
+ if (!enableVerification) {
+ return
+ }
+ singleDao(
+ """
+ @Dao interface MyDao {
+ static class Merged extends User {
+ @Relation(parentColumn = "name", entityColumn = "lastName",
+ entity = User.class)
+ java.util.List<User> users;
+ }
+ @Transaction
+ @Query("select * from user")
+ abstract java.util.List<Merged> loadUsers();
+ }
+ """
+ ) { dao, _ ->
+ // test sanity
+ assertThat(dao.queryMethods.size, `is`(1))
+ assertThat(dao.queryMethods.first().inTransaction, `is`(true))
+ }.compilesWithoutError()
+ .withWarningCount(0)
+ }
+
+ fun singleDao(vararg inputs: String, handler: (Dao, TestInvocation) -> Unit):
+ CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyDao",
+ DAO_PREFIX + inputs.joinToString("\n")
+ ), COMMON.USER))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(androidx.room.Dao::class,
+ androidx.room.Entity::class,
+ androidx.room.Relation::class,
+ androidx.room.Transaction::class,
+ androidx.room.ColumnInfo::class,
+ androidx.room.PrimaryKey::class,
+ androidx.room.Query::class)
+ .nextRunHandler { invocation ->
+ val dao = invocation.roundEnv
+ .getElementsAnnotatedWith(
+ androidx.room.Dao::class.java)
+ .first()
+ val dbVerifier = if (enableVerification) {
+ createVerifierFromEntities(invocation)
+ } else {
+ null
+ }
+ val dbType = MoreTypes.asDeclared(
+ invocation.context.processingEnv.elementUtils
+ .getTypeElement(RoomTypeNames.ROOM_DB.toString())
+ .asType())
+ val parser = DaoProcessor(invocation.context,
+ MoreElements.asType(dao), dbType, dbVerifier)
+
+ val parsedDao = parser.process()
+ handler(parsedDao, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
new file mode 100644
index 0000000..8f2231b
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -0,0 +1,712 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import COMMON
+import androidx.room.RoomProcessor
+import androidx.room.solver.query.result.EntityRowAdapter
+import androidx.room.solver.query.result.PojoRowAdapter
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.Database
+import androidx.room.vo.Warning
+import com.google.auto.common.MoreElements
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ClassName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.sameInstance
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.tools.JavaFileObject
+import javax.tools.StandardLocation
+
+@RunWith(JUnit4::class)
+class DatabaseProcessorTest {
+ companion object {
+ const val DATABASE_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ """
+ val DB1: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Db1",
+ """
+ $DATABASE_PREFIX
+ @Database(entities = {Book.class}, version = 42)
+ public abstract class Db1 extends RoomDatabase {
+ abstract BookDao bookDao();
+ }
+ """)
+ val DB2: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Db2",
+ """
+ $DATABASE_PREFIX
+ @Database(entities = {Book.class}, version = 42)
+ public abstract class Db2 extends RoomDatabase {
+ abstract BookDao bookDao();
+ }
+ """)
+ val DB3: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Db3",
+ """
+ $DATABASE_PREFIX
+ @Database(entities = {Book.class}, version = 42)
+ public abstract class Db3 extends RoomDatabase {
+ }
+ """)
+ val USER: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.User",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class User {
+ @PrimaryKey
+ int uid;
+ }
+ """)
+ val USER_DAO: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.UserDao",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Dao
+ public interface UserDao {
+ @Query("SELECT * FROM user")
+ public java.util.List<User> loadAll();
+
+ @Insert
+ public void insert(User... users);
+
+ @Query("SELECT * FROM user where uid = :uid")
+ public User loadOne(int uid);
+
+ @TypeConverters(Converter.class)
+ @Query("SELECT * FROM user where uid = :uid")
+ public User loadWithConverter(int uid);
+
+ @Query("SELECT * FROM user where uid = :uid")
+ public Pojo loadOnePojo(int uid);
+
+ @Query("SELECT * FROM user")
+ public java.util.List<Pojo> loadAllPojos();
+
+ @TypeConverters(Converter.class)
+ @Query("SELECT * FROM user where uid = :uid")
+ public Pojo loadPojoWithConverter(int uid);
+
+ public static class Converter {
+ @TypeConverter
+ public static java.util.Date foo(Long input) {return null;}
+ }
+
+ public static class Pojo {
+ public int uid;
+ }
+ }
+ """)
+ val BOOK: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.Book",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class Book {
+ @PrimaryKey
+ int bookId;
+ }
+ """)
+ val BOOK_DAO: JavaFileObject = JavaFileObjects.forSourceString("foo.bar.BookDao",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Dao
+ public interface BookDao {
+ @Query("SELECT * FROM book")
+ public java.util.List<Book> loadAllBooks();
+ @Insert
+ public void insert(Book book);
+ }
+ """)
+ }
+
+ @Test
+ fun simple() {
+ singleDb("""
+ @Database(entities = {User.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract UserDao userDao();
+ }
+ """, USER, USER_DAO) { db, _ ->
+ assertThat(db.daoMethods.size, `is`(1))
+ assertThat(db.entities.size, `is`(1))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun multiple() {
+ singleDb("""
+ @Database(entities = {User.class, Book.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract UserDao userDao();
+ abstract BookDao bookDao();
+ }
+ """, USER, USER_DAO, BOOK, BOOK_DAO) { db, _ ->
+ assertThat(db.daoMethods.size, `is`(2))
+ assertThat(db.entities.size, `is`(2))
+ assertThat(db.daoMethods.map { it.name }, `is`(listOf("userDao", "bookDao")))
+ assertThat(db.entities.map { it.type.toString() },
+ `is`(listOf("foo.bar.User", "foo.bar.Book")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun detectMissingBaseClass() {
+ singleDb("""
+ @Database(entities = {User.class, Book.class}, version = 42)
+ public abstract class MyDb {
+ }
+ """, USER, BOOK) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
+ }
+
+ @Test
+ fun detectMissingTable() {
+ singleDb(
+ """
+ @Database(entities = {Book.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract BookDao bookDao();
+ }
+ """, BOOK, JavaFileObjects.forSourceString("foo.bar.BookDao",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Dao
+ public interface BookDao {
+ @Query("SELECT * FROM nonExistentTable")
+ public java.util.List<Book> loadAllBooks();
+ }
+ """)) { _, _ ->
+ }.failsToCompile().withErrorContaining("no such table: nonExistentTable")
+ }
+
+ @Test
+ fun detectDuplicateTableNames() {
+ singleDb("""
+ @Database(entities = {User.class, AnotherClass.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract UserDao userDao();
+ }
+ """, USER, USER_DAO, JavaFileObjects.forSourceString("foo.bar.AnotherClass",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(tableName="user")
+ public class AnotherClass {
+ @PrimaryKey
+ int uid;
+ }
+ """)) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.duplicateTableNames("user",
+ listOf("foo.bar.User", "foo.bar.AnotherClass"))
+ )
+ }
+
+ @Test
+ fun skipBadQueryVerification() {
+ singleDb(
+ """
+ @SkipQueryVerification
+ @Database(entities = {Book.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract BookDao bookDao();
+ }
+ """, BOOK, JavaFileObjects.forSourceString("foo.bar.BookDao",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Dao
+ public interface BookDao {
+ @Query("SELECT nonExistingField FROM Book")
+ public java.util.List<Book> loadAllBooks();
+ }
+ """)) { _, _ ->
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun multipleDatabases() {
+ val db1_2 = JavaFileObjects.forSourceString("foo.barx.Db1",
+ """
+ package foo.barx;
+ import androidx.room.*;
+ import foo.bar.*;
+ @Database(entities = {Book.class}, version = 42)
+ public abstract class Db1 extends RoomDatabase {
+ abstract BookDao bookDao();
+ }
+ """)
+ Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(BOOK, BOOK_DAO, DB1, DB2, db1_2))
+ .processedWith(RoomProcessor())
+ .compilesWithoutError()
+ .and()
+ .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar", "Db1_Impl.class")
+ .and()
+ .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar", "Db2_Impl.class")
+ .and()
+ .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.barx", "Db1_Impl.class")
+ .and()
+ .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar",
+ "BookDao_Db1_0_Impl.class")
+ .and()
+ .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar",
+ "BookDao_Db1_1_Impl.class")
+ .and()
+ .generatesFileNamed(StandardLocation.CLASS_OUTPUT, "foo.bar",
+ "BookDao_Db2_Impl.class")
+ }
+
+ @Test
+ fun twoDaoMethodsForTheSameDao() {
+ singleDb(
+ """
+ @Database(entities = {User.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract UserDao userDao();
+ abstract UserDao userDao2();
+ }
+ """, USER, USER_DAO) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.DAO_METHOD_CONFLICTS_WITH_OTHERS)
+ .and()
+ .withErrorContaining(ProcessorErrors.duplicateDao(
+ ClassName.get("foo.bar", "UserDao"), listOf("userDao", "userDao2")
+ ))
+ }
+
+ @Test
+ fun suppressedWarnings() {
+ singleDb(
+ """
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Database(entities = {User.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract UserDao userDao();
+ }
+ """, USER, USER_DAO) { db, invocation ->
+ assertThat(DatabaseProcessor(invocation.context, db.element)
+ .context.logger.suppressedWarnings, `is`(setOf(Warning.CURSOR_MISMATCH)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun duplicateIndexNames() {
+ val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = {@Index(name ="index_name", value = {"name"})})
+ public class Entity1 {
+ @PrimaryKey
+ int uid;
+ String name;
+ }
+ """)
+
+ val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = {@Index(name ="index_name", value = {"anotherName"})})
+ public class Entity2 {
+ @PrimaryKey
+ int uid;
+ String anotherName;
+ }
+ """)
+ singleDb("""
+ @Database(entities = {Entity1.class, Entity2.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ }
+ """, entity1, entity2) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.duplicateIndexInDatabase("index_name",
+ listOf("foo.bar.Entity1 > index_name", "foo.bar.Entity2 > index_name"))
+ )
+ }
+
+ @Test
+ fun foreignKey_missingParent() {
+ val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(foreignKeys = @ForeignKey(entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name"))
+ public class Entity1 {
+ @PrimaryKey
+ int uid;
+ String name;
+ }
+ """)
+ singleDb("""
+ @Database(entities = {Entity1.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ }
+ """, entity1, COMMON.USER) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.foreignKeyMissingParentEntityInDatabase("User", "foo.bar.Entity1")
+ )
+ }
+
+ @Test
+ fun foreignKey_missingParentIndex() {
+ val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(foreignKeys = @ForeignKey(entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name"))
+ public class Entity1 {
+ @PrimaryKey
+ int uid;
+ String name;
+ }
+ """)
+ singleDb("""
+ @Database(entities = {Entity1.class, User.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ }
+ """, entity1, COMMON.USER) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.foreignKeyMissingIndexInParent(
+ parentEntity = COMMON.USER_TYPE_NAME.toString(),
+ parentColumns = listOf("lastName"),
+ childEntity = "foo.bar.Entity1",
+ childColumns = listOf("name")
+ )
+ )
+ }
+
+ @Test
+ fun foreignKey_goodWithPrimaryKey() {
+ val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
+ parentColumns = "uid",
+ childColumns = "parentId"))
+ public class Entity1 {
+ @PrimaryKey
+ int uid;
+ int parentId;
+ String name;
+ }
+ """)
+
+ val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class Entity2 {
+ @PrimaryKey
+ int uid;
+ String anotherName;
+ }
+ """)
+ singleDb("""
+ @Database(entities = {Entity1.class, Entity2.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ }
+ """, entity1, entity2) { _, _ ->
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun foreignKey_missingParentColumn() {
+ val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
+ parentColumns = {"anotherName", "anotherName2"},
+ childColumns = {"name", "name2"}))
+ public class Entity1 {
+ @PrimaryKey
+ int uid;
+ String name;
+ String name2;
+ }
+ """)
+
+ val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class Entity2 {
+ @PrimaryKey
+ int uid;
+ String anotherName;
+ }
+ """)
+ singleDb("""
+ @Database(entities = {Entity1.class, Entity2.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ }
+ """, entity1, entity2) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.foreignKeyParentColumnDoesNotExist("foo.bar.Entity2",
+ "anotherName2", listOf("uid", "anotherName"))
+ )
+ }
+
+ @Test
+ fun foreignKey_goodWithIndex() {
+ val entity1 = JavaFileObjects.forSourceString("foo.bar.Entity1",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
+ parentColumns = {"anotherName", "anotherName2"},
+ childColumns = {"name", "name2"}))
+ public class Entity1 {
+ @PrimaryKey
+ int uid;
+ String name;
+ String name2;
+ }
+ """)
+
+ val entity2 = JavaFileObjects.forSourceString("foo.bar.Entity2",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = @Index(value = {"anotherName2", "anotherName"}, unique = true))
+ public class Entity2 {
+ @PrimaryKey
+ int uid;
+ String anotherName;
+ String anotherName2;
+ }
+ """)
+ singleDb("""
+ @Database(entities = {Entity1.class, Entity2.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ }
+ """, entity1, entity2) { _, _ ->
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertNotAReferencedEntity() {
+ singleDb("""
+ @Database(entities = {User.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ abstract BookDao bookDao();
+ }
+ """, USER, USER_DAO, BOOK, BOOK_DAO) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.shortcutEntityIsNotInDatabase(
+ database = "foo.bar.MyDb",
+ dao = "foo.bar.BookDao",
+ entity = "foo.bar.Book"
+ )
+ )
+ }
+
+ @Test
+ fun cache_entity() {
+ singleDb("""
+ @Database(entities = {User.class}, version = 42)
+ @SkipQueryVerification
+ public abstract class MyDb extends RoomDatabase {
+ public abstract MyUserDao userDao();
+ @Dao
+ interface MyUserDao {
+ @Insert
+ public void insert(User... users);
+
+ @Query("SELECT * FROM user where uid = :uid")
+ public User loadOne(int uid);
+
+ @TypeConverters(Converter.class)
+ @Query("SELECT * FROM user where uid = :uid")
+ public User loadWithConverter(int uid);
+ }
+ public static class Converter {
+ @TypeConverter
+ public static java.util.Date foo(Long input) {return null;}
+ }
+ }
+ """, USER, USER_DAO) { db, _ ->
+ val userDao = db.daoMethods.first().dao
+ val insertionMethod = userDao.insertionMethods.find { it.name == "insert" }
+ assertThat(insertionMethod, notNullValue())
+ val loadOne = userDao.queryMethods.find { it.name == "loadOne" }
+ assertThat(loadOne, notNullValue())
+ val adapter = loadOne?.queryResultBinder?.adapter?.rowAdapter
+ assertThat("test sanity", adapter, instanceOf(EntityRowAdapter::class.java))
+ val adapterEntity = (adapter as EntityRowAdapter).entity
+ assertThat(insertionMethod?.entities?.values?.first(), sameInstance(adapterEntity))
+
+ val withConverter = userDao.queryMethods.find { it.name == "loadWithConverter" }
+ assertThat(withConverter, notNullValue())
+ val convAdapter = withConverter?.queryResultBinder?.adapter?.rowAdapter
+ assertThat("test sanity", adapter, instanceOf(EntityRowAdapter::class.java))
+ val convAdapterEntity = (convAdapter as EntityRowAdapter).entity
+ assertThat(insertionMethod?.entities?.values?.first(),
+ not(sameInstance(convAdapterEntity)))
+
+ assertThat(convAdapterEntity, notNullValue())
+ assertThat(adapterEntity, notNullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun cache_pojo() {
+ singleDb("""
+ @Database(entities = {User.class}, version = 42)
+ public abstract class MyDb extends RoomDatabase {
+ public abstract UserDao userDao();
+ }
+ """, USER, USER_DAO) { db, _ ->
+ val userDao = db.daoMethods.first().dao
+ val loadOne = userDao.queryMethods.find { it.name == "loadOnePojo" }
+ assertThat(loadOne, notNullValue())
+ val adapter = loadOne?.queryResultBinder?.adapter?.rowAdapter
+ assertThat("test sanity", adapter, instanceOf(PojoRowAdapter::class.java))
+ val adapterPojo = (adapter as PojoRowAdapter).pojo
+
+ val loadAll = userDao.queryMethods.find { it.name == "loadAllPojos" }
+ assertThat(loadAll, notNullValue())
+ val loadAllAdapter = loadAll?.queryResultBinder?.adapter?.rowAdapter
+ assertThat("test sanity", loadAllAdapter, instanceOf(PojoRowAdapter::class.java))
+ val loadAllPojo = (loadAllAdapter as PojoRowAdapter).pojo
+ assertThat(adapter, not(sameInstance(loadAllAdapter)))
+ assertThat(adapterPojo, sameInstance(loadAllPojo))
+
+ val withConverter = userDao.queryMethods.find { it.name == "loadPojoWithConverter" }
+ assertThat(withConverter, notNullValue())
+ val convAdapter = withConverter?.queryResultBinder?.adapter?.rowAdapter
+ assertThat("test sanity", adapter, instanceOf(PojoRowAdapter::class.java))
+ val convAdapterPojo = (convAdapter as PojoRowAdapter).pojo
+ assertThat(convAdapterPojo, notNullValue())
+ assertThat(convAdapterPojo, not(sameInstance(adapterPojo)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun daoConstructor_RoomDatabase() {
+ assertConstructor(listOf(DB1), "BookDao(RoomDatabase db) {}")
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun daoConstructor_specificDatabase() {
+ assertConstructor(listOf(DB1), "BookDao(Db1 db) {}")
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun daoConstructor_wrongDatabase() {
+ assertConstructor(listOf(DB1, DB3), "BookDao(Db3 db) {}")
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors
+ .daoMustHaveMatchingConstructor("foo.bar.BookDao", "foo.bar.Db1"))
+ }
+
+ @Test
+ fun daoConstructor_multipleDatabases_RoomDatabase() {
+ assertConstructor(listOf(DB1, DB2), "BookDao(RoomDatabase db) {}")
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun daoConstructor_multipleDatabases_specificDatabases() {
+ assertConstructor(listOf(DB1, DB2), """
+ BookDao(Db1 db) {}
+ BookDao(Db2 db) {}
+ """)
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun daoConstructor_multipleDatabases_empty() {
+ assertConstructor(listOf(DB1, DB2), """
+ BookDao(Db1 db) {}
+ BookDao() {} // Db2 uses this
+ """)
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun daoConstructor_multipleDatabases_noMatch() {
+ assertConstructor(listOf(DB1, DB2), """
+ BookDao(Db1 db) {}
+ """)
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors
+ .daoMustHaveMatchingConstructor("foo.bar.BookDao", "foo.bar.Db2"))
+ }
+
+ fun assertConstructor(dbs: List<JavaFileObject>, constructor: String): CompileTester {
+ val bookDao = JavaFileObjects.forSourceString("foo.bar.BookDao",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Dao
+ public abstract class BookDao {
+ $constructor
+ }
+ """)
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(BOOK, bookDao) + dbs)
+ .processedWith(RoomProcessor())
+ }
+
+ fun singleDb(input: String, vararg otherFiles: JavaFileObject,
+ handler: (Database, TestInvocation) -> Unit): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(otherFiles.toMutableList()
+ + JavaFileObjects.forSourceString("foo.bar.MyDb",
+ DATABASE_PREFIX + input
+ ))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(androidx.room.Database::class)
+ .nextRunHandler { invocation ->
+ val entity = invocation.roundEnv
+ .getElementsAnnotatedWith(
+ androidx.room.Database::class.java)
+ .first()
+ val parser = DatabaseProcessor(invocation.context,
+ MoreElements.asType(entity))
+ val parsedDb = parser.process()
+ handler(parsedDb, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DeletionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DeletionMethodProcessorTest.kt
new file mode 100644
index 0000000..36f0e4a
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DeletionMethodProcessorTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Delete
+import androidx.room.processor.ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
+import androidx.room.processor.ProcessorErrors.DELETION_MISSING_PARAMS
+import androidx.room.vo.DeletionMethod
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class DeletionMethodProcessorTest : ShortcutMethodProcessorTest<DeletionMethod>(Delete::class) {
+ override fun invalidReturnTypeError(): String = DELETION_METHODS_MUST_RETURN_VOID_OR_INT
+
+ override fun noParamsError(): String = DELETION_MISSING_PARAMS
+
+ override fun process(baseContext: Context, containing: DeclaredType,
+ executableElement: ExecutableElement): DeletionMethod {
+ return DeletionMethodProcessor(baseContext, containing, executableElement).process()
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
new file mode 100644
index 0000000..ad7e072
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.vo.CallType
+import androidx.room.vo.Field
+import androidx.room.vo.FieldGetter
+import androidx.room.vo.FieldSetter
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import javax.lang.model.type.TypeKind.INT
+
+@RunWith(Parameterized::class)
+class EntityNameMatchingVariationsTest(triple: Triple<String, String, String>) :
+ BaseEntityParserTest() {
+ val fieldName = triple.first
+ val getterName = triple.second
+ val setterName = triple.third
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params(): List<Triple<String, String, String>> {
+ val result = arrayListOf<Triple<String, String, String>>()
+ arrayListOf("x", "_x", "mX").forEach { field ->
+ arrayListOf("getX", "x").forEach { getter ->
+ arrayListOf("setX", "x").forEach { setter ->
+ result.add(Triple(field, getter, setter))
+ }
+ }
+ }
+ return result
+ }
+ }
+
+ @Test
+ fun testSuccessfulParamToMethodMatching() {
+ singleEntity("""
+ @PrimaryKey
+ private int $fieldName;
+ public int $getterName() { return $fieldName; }
+ public void $setterName(int id) { this.$fieldName = id; }
+ """) { entity, invocation ->
+ assertThat(entity.type.toString(), `is`("foo.bar.MyEntity"))
+ assertThat(entity.fields.size, `is`(1))
+ val field = entity.fields.first()
+ val intType = invocation.processingEnv.typeUtils.getPrimitiveType(INT)
+ assertThat(field, `is`(Field(
+ element = field.element,
+ name = fieldName,
+ type = intType,
+ columnName = fieldName,
+ affinity = SQLTypeAffinity.INTEGER)))
+ assertThat(field.setter, `is`(FieldSetter(setterName, intType, CallType.METHOD)))
+ assertThat(field.getter, `is`(FieldGetter(getterName, intType, CallType.METHOD)))
+ }.compilesWithoutError()
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/EntityProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/EntityProcessorTest.kt
new file mode 100644
index 0000000..69ada7c
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/EntityProcessorTest.kt
@@ -0,0 +1,1907 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import COMMON
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.processor.ProcessorErrors.RELATION_IN_ENTITY
+import androidx.room.vo.CallType
+import androidx.room.vo.Field
+import androidx.room.vo.FieldGetter
+import androidx.room.vo.FieldSetter
+import androidx.room.vo.Index
+import androidx.room.vo.Pojo
+import com.google.testing.compile.JavaFileObjects
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.lang.model.type.TypeKind.INT
+
+@RunWith(JUnit4::class)
+class EntityProcessorTest : BaseEntityParserTest() {
+ @Test
+ fun simple() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public int getId() { return id; }
+ public void setId(int id) { this.id = id; }
+ """) { entity, invocation ->
+ assertThat(entity.type.toString(), `is`("foo.bar.MyEntity"))
+ assertThat(entity.fields.size, `is`(1))
+ val field = entity.fields.first()
+ val intType = invocation.processingEnv.typeUtils.getPrimitiveType(INT)
+ assertThat(field, `is`(Field(
+ element = field.element,
+ name = "id",
+ type = intType,
+ columnName = "id",
+ affinity = SQLTypeAffinity.INTEGER)))
+ assertThat(field.setter, `is`(FieldSetter("setId", intType, CallType.METHOD)))
+ assertThat(field.getter, `is`(FieldGetter("getId", intType, CallType.METHOD)))
+ assertThat(entity.primaryKey.fields, `is`(listOf(field)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun noGetter() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {this.id = id;}
+ """) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
+ }
+
+ @Test
+ fun getterWithBadType() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public float getId() {return 0f;}
+ public void setId(int id) {this.id = id;}
+ """) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD)
+ }
+
+ @Test
+ fun setterWithBadType() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public int getId() {return id;}
+ public void setId(float id) {}
+ """) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
+ }
+
+ @Test
+ fun setterWithAssignableType() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public int getId() {return id;}
+ public void setId(Integer id) {}
+ """) { _, _ -> }
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun getterWithAssignableType() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public Integer getId() {return id;}
+ public void setId(int id) {}
+ """) { _, _ -> }
+ .compilesWithoutError()
+ }
+
+ @Test
+ fun noSetter() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public int getId(){ return id; }
+ """) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.CANNOT_FIND_SETTER_FOR_FIELD)
+ }
+
+ @Test
+ fun tooManyGetters() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {}
+ public int getId(){ return id; }
+ public int id(){ return id; }
+ """) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining("getId, id")
+ }
+
+ @Test
+ fun tooManyGettersWithIgnore() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {}
+ public int getId(){ return id; }
+ @Ignore public int id(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().getter.name, `is`("getId"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun tooManyGettersWithDifferentVisibility() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {}
+ public int getId(){ return id; }
+ protected int id(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().getter.name, `is`("getId"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun tooManyGettersWithDifferentTypes() {
+ singleEntity("""
+ @PrimaryKey
+ public int id;
+ public void setId(int id) {}
+ public int getId(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().getter.name, `is`("id"))
+ assertThat(entity.fields.first().getter.callType, `is`(CallType.FIELD))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun tooManySetters() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {}
+ public void id(int id) {}
+ public int getId(){ return id; }
+ """) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining("setId, id")
+ }
+
+ @Test
+ fun tooManySettersWithIgnore() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {}
+ @Ignore public void id(int id) {}
+ public int getId(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().setter.name, `is`("setId"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun tooManySettersWithDifferentVisibility() {
+ singleEntity("""
+ @PrimaryKey
+ private int id;
+ public void setId(int id) {}
+ protected void id(int id) {}
+ public int getId(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().setter.name, `is`("setId"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun tooManySettersWithDifferentTypes() {
+ singleEntity("""
+ @PrimaryKey
+ public int id;
+ public void setId(int id) {}
+ public int getId(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().setter.name, `is`("id"))
+ assertThat(entity.fields.first().setter.callType, `is`(CallType.FIELD))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun preferPublicOverProtected() {
+ singleEntity("""
+ @PrimaryKey
+ int id;
+ public void setId(int id) {}
+ public int getId(){ return id; }
+ """) { entity, _ ->
+ assertThat(entity.fields.first().setter.name, `is`("setId"))
+ assertThat(entity.fields.first().getter.name, `is`("getId"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun customName() {
+ singleEntity("""
+ @PrimaryKey
+ int x;
+ """, hashMapOf(Pair("tableName", "\"foo_table\""))) { entity, _ ->
+ assertThat(entity.tableName, `is`("foo_table"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun emptyCustomName() {
+ singleEntity("""
+ @PrimaryKey
+ int x;
+ """, hashMapOf(Pair("tableName", "\" \""))) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.ENTITY_TABLE_NAME_CANNOT_BE_EMPTY)
+ }
+
+ @Test
+ fun missingPrimaryKey() {
+ singleEntity("""
+ """) { _, _ ->
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.MISSING_PRIMARY_KEY)
+ }
+
+ @Test
+ fun missingColumnAdapter() {
+ singleEntity("""
+ @PrimaryKey
+ public java.util.Date myDate;
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER)
+ }
+
+ @Test
+ fun dropSubPrimaryKey() {
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ @Embedded
+ Point myPoint;
+ static class Point {
+ @PrimaryKey
+ int x;
+ int y;
+ }
+ """
+ ) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id")))
+ }.compilesWithoutError()
+ .withWarningCount(1)
+ .withWarningContaining(ProcessorErrors.embeddedPrimaryKeyIsDropped(
+ "foo.bar.MyEntity", "x"))
+ }
+
+ @Test
+ fun ignoreDropSubPrimaryKey() {
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ @Embedded
+ @SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED)
+ Point myPoint;
+ static class Point {
+ @PrimaryKey
+ int x;
+ int y;
+ }
+ """
+ ) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id")))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun notNull() {
+ singleEntity(
+ """
+ @PrimaryKey int id;
+ @NonNull public String name;
+ """
+ ) { entity, _ ->
+ val field = fieldsByName(entity, "name").first()
+ assertThat(field.name, `is`("name"))
+ assertThat(field.columnName, `is`("name"))
+ assertThat(field.nonNull, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ private fun fieldsByName(entity: Pojo, vararg fieldNames: String): List<Field> {
+ return fieldNames
+ .map { name -> entity.fields.find { it.name == name } }
+ .filterNotNull()
+ }
+
+ @Test
+ fun index_simple() {
+ val annotation = mapOf(
+ "indices" to """@Index("foo")"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(name = "index_MyEntity_foo",
+ unique = false,
+ fields = fieldsByName(entity, "foo")))))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_fromField() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ @ColumnInfo(index = true)
+ public String foo;
+ """) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(name = "index_MyEntity_foo",
+ unique = false,
+ fields = fieldsByName(entity, "foo")))
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_multiColumn() {
+ val annotation = mapOf(
+ "indices" to """@Index({"foo", "id"})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(name = "index_MyEntity_foo_id",
+ unique = false,
+ fields = fieldsByName(entity, "foo", "id")))
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_multiple() {
+ val annotation = mapOf(
+ "indices" to """{@Index({"foo", "id"}), @Index({"bar_column", "foo"})}"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ @ColumnInfo(name = "bar_column")
+ public String bar;
+ """
+ , annotation) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(name = "index_MyEntity_foo_id",
+ unique = false,
+ fields = fieldsByName(entity, "foo", "id")),
+ Index(name = "index_MyEntity_bar_column_foo",
+ unique = false,
+ fields = fieldsByName(entity, "bar", "foo")))
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_unique() {
+ val annotation = mapOf(
+ "indices" to """@Index(value = {"foo", "id"}, unique = true)"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(
+ name = "index_MyEntity_foo_id",
+ unique = true,
+ fields = fieldsByName(entity, "foo", "id")))
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_customName() {
+ val annotation = mapOf(
+ "indices" to """@Index(value = {"foo"}, name = "myName")"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(name = "myName",
+ unique = false,
+ fields = fieldsByName(entity, "foo")))
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_customTableName() {
+ val annotation = mapOf(
+ "tableName" to "\"MyTable\"",
+ "indices" to """@Index(value = {"foo"})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { entity, _ ->
+ assertThat(entity.indices, `is`(
+ listOf(Index(name = "index_MyTable_foo",
+ unique = false,
+ fields = fieldsByName(entity, "foo")))
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun index_empty() {
+ val annotation = mapOf(
+ "indices" to """@Index({})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.INDEX_COLUMNS_CANNOT_BE_EMPTY
+ )
+ }
+
+ @Test
+ fun index_missingColumn() {
+ val annotation = mapOf(
+ "indices" to """@Index({"foo", "bar"})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ public String foo;
+ """
+ , annotation) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.indexColumnDoesNotExist("bar", listOf("id, foo"))
+ )
+ }
+
+ @Test
+ fun index_nameConflict() {
+ val annotation = mapOf(
+ "indices" to """@Index({"foo"})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ @ColumnInfo(index = true)
+ public String foo;
+ """
+ , annotation) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.duplicateIndexInEntity("index_MyEntity_foo")
+ )
+ }
+
+ @Test
+ fun index_droppedParentFieldIndex() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ @ColumnInfo(index = true)
+ String name;
+ String lastName;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """, baseClass = "foo.bar.Base", jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.indices.isEmpty(), `is`(true))
+ }.compilesWithoutError()
+ .withWarningContaining(
+ ProcessorErrors.droppedSuperClassFieldIndex(
+ fieldName = "name",
+ childEntity = "foo.bar.MyEntity",
+ superEntity = "foo.bar.Base")
+ )
+ }
+
+ @Test
+ fun index_keptGrandParentEntityIndex() {
+ val grandParent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = @Index({"name", "lastName"}))
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Parent",
+ """
+ package foo.bar;
+ import androidx.room.*;
+
+ public class Parent extends Base {
+ String iHaveAField;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Parent",
+ attributes = hashMapOf("inheritSuperIndices" to "true"),
+ jfos = listOf(parent, grandParent)) {
+ entity, _ ->
+ assertThat(entity.indices.size, `is`(1))
+ assertThat(entity.indices.first(),
+ `is`(Index(name = "index_MyEntity_name_lastName",
+ unique = false,
+ fields = fieldsByName(entity, "name", "lastName"))))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun index_keptParentEntityIndex() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = @Index({"name", "lastName"}))
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ attributes = hashMapOf("inheritSuperIndices" to "true"),
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.indices.size, `is`(1))
+ assertThat(entity.indices.first(),
+ `is`(Index(name = "index_MyEntity_name_lastName",
+ unique = false,
+ fields = fieldsByName(entity, "name", "lastName"))))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun index_keptParentFieldIndex() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ @ColumnInfo(index = true)
+ String name;
+ String lastName;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ attributes = hashMapOf("inheritSuperIndices" to "true"),
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.indices.size, `is`(1))
+ assertThat(entity.indices.first(),
+ `is`(Index(name = "index_MyEntity_name",
+ unique = false,
+ fields = fieldsByName(entity, "name"))))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun index_droppedGrandParentEntityIndex() {
+ val grandParent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = @Index({"name", "lastName"}))
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Parent",
+ """
+ package foo.bar;
+ import androidx.room.*;
+
+ public class Parent extends Base {
+ String iHaveAField;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """, baseClass = "foo.bar.Parent", jfos = listOf(parent, grandParent)) {
+ entity, _ ->
+ assertThat(entity.indices.isEmpty(), `is`(true))
+ }.compilesWithoutError()
+ .withWarningContaining(
+ ProcessorErrors.droppedSuperClassIndex(
+ childEntity = "foo.bar.MyEntity",
+ superEntity = "foo.bar.Base")
+ )
+ }
+
+ @Test
+ fun index_droppedParentEntityIndex() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(indices = @Index({"name", "lastName"}))
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """, baseClass = "foo.bar.Base", jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.indices.isEmpty(), `is`(true))
+ }.compilesWithoutError()
+ .withWarningContaining(
+ ProcessorErrors.droppedSuperClassIndex(
+ childEntity = "foo.bar.MyEntity",
+ superEntity = "foo.bar.Base")
+ )
+ }
+
+ @Test
+ fun index_droppedEmbeddedEntityIndex() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ @Embedded
+ public Foo foo;
+ @Entity(indices = {@Index("a")})
+ static class Foo {
+ @PrimaryKey
+ @ColumnInfo(name = "foo_id")
+ int id;
+ @ColumnInfo(index = true)
+ public int a;
+ }
+ """) { entity, _ ->
+ assertThat(entity.indices.isEmpty(), `is`(true))
+ }.compilesWithoutError()
+ .withWarningContaining(
+ ProcessorErrors.droppedEmbeddedIndex(
+ entityName = "foo.bar.MyEntity.Foo",
+ fieldPath = "foo",
+ grandParent = "foo.bar.MyEntity")
+ )
+ }
+
+ @Test
+ fun index_onEmbeddedField() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ @Embedded
+ @ColumnInfo(index = true)
+ public Foo foo;
+ static class Foo {
+ @ColumnInfo(index = true)
+ public int a;
+ }
+ """) { entity, _ ->
+ assertThat(entity.indices.isEmpty(), `is`(true))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION
+ )
+ }
+
+ @Test
+ fun index_droppedEmbeddedFieldIndex() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ @Embedded
+ public Foo foo;
+ static class Foo {
+ @ColumnInfo(index = true)
+ public int a;
+ }
+ """) { entity, _ ->
+ assertThat(entity.indices.isEmpty(), `is`(true))
+ }.compilesWithoutError()
+ .withWarningContaining(
+ ProcessorErrors.droppedEmbeddedFieldIndex("foo > a", "foo.bar.MyEntity")
+ )
+ }
+
+ @Test
+ fun index_referenceEmbeddedField() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ @Embedded
+ public Foo foo;
+ static class Foo {
+ public int a;
+ }
+ """, attributes = mapOf("indices" to "@Index(\"a\")")) { entity, _ ->
+ assertThat(entity.indices.size, `is`(1))
+ assertThat(entity.indices.first(), `is`(
+ Index(
+ name = "index_MyEntity_a",
+ unique = false,
+ fields = fieldsByName(entity, "a")
+ )
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primaryKey_definedInBothWays() {
+ singleEntity(
+ """
+ public int id;
+ @PrimaryKey
+ public String foo;
+ """,
+ attributes = mapOf("primaryKeys" to "\"id\"")) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.multiplePrimaryKeyAnnotations(
+ listOf("PrimaryKey[id]", "PrimaryKey[foo]")
+ ))
+ }
+
+ @Test
+ fun primaryKey_badColumnName() {
+ singleEntity(
+ """
+ public int id;
+ """,
+ attributes = mapOf("primaryKeys" to "\"foo\"")) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.primaryKeyColumnDoesNotExist("foo", listOf("id")))
+ }
+
+ @Test
+ fun primaryKey_multipleAnnotations() {
+ singleEntity("""
+ @PrimaryKey
+ int x;
+ @PrimaryKey
+ int y;
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.isEmpty(), `is`(true))
+ }.failsToCompile()
+ .withErrorContaining(
+ ProcessorErrors.multiplePrimaryKeyAnnotations(
+ listOf("PrimaryKey[x]", "PrimaryKey[y]")))
+ }
+
+ @Test
+ fun primaryKey_fromParentField() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("baseId"))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun primaryKey_fromParentEntity() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(primaryKeys = "baseId")
+ public class Base {
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("baseId"))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun primaryKey_overrideFromParentField() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ public class Base {
+ @PrimaryKey
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ assertThat(entity.primaryKey.autoGenerateId, `is`(false))
+ }.compilesWithoutError().withNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[id]"
+ )
+ }
+
+ @Test
+ fun primaryKey_overrideFromParentEntityViaField() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(primaryKeys = "baseId")
+ public class Base {
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ }.compilesWithoutError().withNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[id]"
+ )
+ }
+
+ @Test
+ fun primaryKey_overrideFromParentEntityViaEntity() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity(primaryKeys = "baseId")
+ public class Base {
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent),
+ attributes = mapOf("primaryKeys" to "\"id\"")) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ assertThat(entity.primaryKey.autoGenerateId, `is`(false))
+ }.compilesWithoutError().withNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[id]"
+ )
+ }
+
+ @Test
+ fun primaryKey_autoGenerate() {
+ listOf("long", "Long", "Integer", "int").forEach { type ->
+ singleEntity(
+ """
+ @PrimaryKey(autoGenerate = true)
+ public $type id;
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ assertThat(entity.primaryKey.autoGenerateId, `is`(true))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun primaryKey_nonNull_notNeeded() {
+ listOf("long", "Long", "Integer", "int").forEach { type ->
+ singleEntity(
+ """
+ @PrimaryKey
+ public $type id;
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ assertThat(entity.primaryKey.autoGenerateId, `is`(false))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun primaryKey_autoGenerateBadType() {
+ listOf("String", "float", "Float", "Double", "double").forEach { type ->
+ singleEntity(
+ """
+ @PrimaryKey(autoGenerate = true)
+ public $type id;
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ assertThat(entity.primaryKey.autoGenerateId, `is`(true))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.AUTO_INCREMENTED_PRIMARY_KEY_IS_NOT_INT)
+ }
+ }
+
+ @Test
+ fun primaryKey_embedded() {
+ singleEntity(
+ """
+ public int id;
+
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ @NonNull
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.columnName },
+ `is`(listOf("bar_a", "bar_b")))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun primaryKey_embeddedInherited() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+
+ public class Base {
+ long baseId;
+ String name, lastName;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ @NonNull
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.columnName },
+ `is`(listOf("bar_a", "bar_b")))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun primaryKey_overrideViaEmbedded() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+
+ @Entity(primaryKeys = "baseId")
+ public class Base {
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ @NonNull
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.columnName },
+ `is`(listOf("bar_a", "bar_b")))
+ }.compilesWithoutError().withNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]")
+ }
+
+ @Test
+ fun primaryKey_overrideEmbedded() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+
+ public class Base {
+ long baseId;
+ String name, lastName;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ @NonNull
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.columnName },
+ `is`(listOf("id")))
+ }.compilesWithoutError().withNoteContaining(
+ "PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
+ }
+
+ @Test
+ fun primaryKey_NonNull() {
+ singleEntity(
+ """
+ @PrimaryKey
+ @NonNull
+ public String id;
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primaryKey_Nullable() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public String id;
+ """) { entity, _ ->
+ assertThat(entity.primaryKey.fields.size, `is`(1))
+ assertThat(entity.primaryKey.fields.firstOrNull()?.name, `is`("id"))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("id"))
+ }
+
+ @Test
+ fun primaryKey_MultipleNullable() {
+ singleEntity(
+ """
+ @PrimaryKey
+ public String id;
+ @PrimaryKey
+ public String anotherId;
+ """) { entity, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("id"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("anotherId"))
+ }
+
+ @Test
+ fun primaryKey_MultipleNullableAndNonNullable() {
+ singleEntity(
+ """
+ @PrimaryKey
+ @NonNull
+ public String id;
+ @PrimaryKey
+ public String anotherId;
+ """) { entity, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("anotherId"))
+ }
+
+ @Test
+ fun primaryKey_definedAsAttributesNullable() {
+ singleEntity(
+ """
+ public int id;
+ public String foo;
+ """,
+ attributes = mapOf("primaryKeys" to "{\"id\", \"foo\"}")) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ }
+
+ @Test
+ fun primaryKey_definedAsAttributesNonNull() {
+ singleEntity(
+ """
+ public int id;
+ @NonNull
+ public String foo;
+ """,
+ attributes = mapOf("primaryKeys" to "{\"id\", \"foo\"}")) { entity, _ ->
+ assertThat(entity.primaryKey.fields.map { it.name }, `is`(listOf("id", "foo")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primaryKey_nullableEmbedded() {
+ singleEntity(
+ """
+ public int id;
+
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ }
+
+ @Test
+ fun primaryKey_nullableEmbeddedObject() {
+ singleEntity(
+ """
+ public int id;
+
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public String a;
+ public String b;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .and().withErrorCount(3)
+ }
+
+ @Test
+ fun primaryKey_nullableEmbeddedObject_multipleParents() {
+ singleEntity(
+ """
+ public int id;
+
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ @Embedded(prefix = "baz_")
+ public Baz a;
+ public String b;
+
+ static class Baz {
+ public Integer bb;
+ }
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a > bb"))
+ .and().withErrorCount(4)
+ }
+
+ @Test
+ fun primaryKey_nullableEmbeddedInherited() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+
+ public class Base {
+ long baseId;
+ String name, lastName;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ .and().withErrorCount(3)
+ }
+
+ @Test
+ fun primaryKey_nullableOverrideViaEmbedded() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.room.*;
+
+ @Entity(primaryKeys = "baseId")
+ public class Base {
+ long baseId;
+ String name, lastName;
+ }
+ """)
+ singleEntity(
+ """
+ public int id;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ .and().withNoteContaining(
+ "PrimaryKey[baseId] is overridden by PrimaryKey[foo > a, foo > b]")
+ .and().withErrorCount(3)
+ }
+
+ @Test
+ fun primaryKey_nullableOverrideEmbedded() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+
+ public class Base {
+ long baseId;
+ String name, lastName;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public int a;
+ public int b;
+ }
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > b"))
+ .and().withNoteContaining(
+ "PrimaryKey[foo > a, foo > b] is overridden by PrimaryKey[id]")
+ .and().withErrorCount(3)
+ }
+
+ @Test
+ fun primaryKey_integerOverrideEmbedded() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+
+ public class Base {
+ long baseId;
+ String name, lastName;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public Integer a;
+ }
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { _, _ ->
+ }.compilesWithoutError().withNoteContaining(
+ "PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
+ }
+
+ @Test
+ fun primaryKey_singleStringPrimaryKeyOverrideEmbedded() {
+ val parent = JavaFileObjects.forSourceLines("foo.bar.Base",
+ """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+
+ public class Base {
+ long baseId;
+ String name, lastName;
+ @Embedded(prefix = "bar_")
+ @PrimaryKey
+ public Foo foo;
+
+ static class Foo {
+ public String a;
+ }
+ }
+ """)
+ singleEntity(
+ """
+ @PrimaryKey
+ public int id;
+ """,
+ baseClass = "foo.bar.Base",
+ jfos = listOf(parent)) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.primaryKeyNull("foo"))
+ .and().withErrorContaining(ProcessorErrors.primaryKeyNull("foo > a"))
+ .and().withNoteContaining(
+ "PrimaryKey[foo > a] is overridden by PrimaryKey[id]")
+ .and().withErrorCount(2)
+ }
+
+ @Test
+ fun relationInEntity() {
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ java.util.List<User> users;
+ """, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(RELATION_IN_ENTITY)
+ }
+
+ @Test
+ fun foreignKey_invalidAction() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name",
+ onDelete = 101
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_FOREIGN_KEY_ACTION)
+ }
+
+ @Test
+ fun foreignKey_badEntity() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = dsa.class,
+ parentColumns = "lastName",
+ childColumns = "name"
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining("cannot find symbol")
+ }
+
+ @Test
+ fun foreignKey_notAnEntity() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.NOT_AN_ENTITY_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name"
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.NOT_AN_ENTITY)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.foreignKeyNotAnEntity(
+ COMMON.NOT_AN_ENTITY_TYPE_NAME.toString()))
+ }
+
+ @Test
+ fun foreignKey_invalidChildColumn() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "namex"
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.foreignKeyChildColumnDoesNotExist(
+ "namex", listOf("id", "name")))
+ }
+
+ @Test
+ fun foreignKey_columnCountMismatch() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = {"name", "id"}
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.foreignKeyColumnNumberMismatch(
+ listOf("name", "id"), listOf("lastName")))
+ }
+
+ @Test
+ fun foreignKey_emptyChildColumns() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = {}
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.FOREIGN_KEY_EMPTY_CHILD_COLUMN_LIST)
+ }
+
+ @Test
+ fun foreignKey_emptyParentColumns() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = {},
+ childColumns = {"name"}
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.FOREIGN_KEY_EMPTY_PARENT_COLUMN_LIST)
+ }
+
+ @Test
+ fun foreignKey_simple() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name",
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE,
+ deferred = true
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { entity, _ ->
+ assertThat(entity.foreignKeys.size, `is`(1))
+ val fKey = entity.foreignKeys.first()
+ assertThat(fKey.parentTable, `is`("User"))
+ assertThat(fKey.parentColumns, `is`(listOf("lastName")))
+ assertThat(fKey.deferred, `is`(true))
+ assertThat(fKey.childFields.size, `is`(1))
+ val field = fKey.childFields.first()
+ assertThat(field.name, `is`("name"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun foreignKey_dontDuplicationChildIndex_SingleColumn() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name",
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE,
+ deferred = true
+ )}""".trimIndent(),
+ "indices" to """@Index("name")"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.compilesWithoutWarnings()
+ }
+
+ @Test
+ fun foreignKey_dontDuplicationChildIndex_MultipleColumns() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = {"lastName", "name"},
+ childColumns = {"lName", "name"},
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE,
+ deferred = true
+ )}""".trimIndent(),
+ "indices" to """@Index({"lName", "name"})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ String lName;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { entity, _ ->
+ assertThat(entity.indices.size, `is`(1))
+ }.compilesWithoutWarnings()
+ }
+
+ @Test
+ fun foreignKey_dontDuplicationChildIndex_WhenCovered() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = {"lastName"},
+ childColumns = {"name"},
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE,
+ deferred = true
+ )}""".trimIndent(),
+ "indices" to """@Index({"name", "lName"})"""
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ String lName;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { entity, _ ->
+ assertThat(entity.indices.size, `is`(1))
+ }.compilesWithoutWarnings()
+ }
+
+ @Test
+ fun foreignKey_warnMissingChildIndex() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "name",
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE,
+ deferred = true
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { entity, _ ->
+ assertThat(entity.indices, `is`(emptyList()))
+ }.compilesWithoutError().withWarningContaining(
+ ProcessorErrors.foreignKeyMissingIndexInChildColumn("name"))
+ }
+
+ @Test
+ fun foreignKey_warnMissingChildrenIndex() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = {"lastName", "name"},
+ childColumns = {"lName", "name"}
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ String lName;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { entity, _ ->
+ assertThat(entity.indices, `is`(emptyList()))
+ }.compilesWithoutError().withWarningContaining(
+ ProcessorErrors.foreignKeyMissingIndexInChildColumns(listOf("lName", "name")))
+ }
+
+ @Test
+ fun foreignKey_dontIndexIfAlreadyPrimaryKey() {
+ val annotation = mapOf(
+ "foreignKeys" to """{@ForeignKey(
+ entity = ${COMMON.USER_TYPE_NAME}.class,
+ parentColumns = "lastName",
+ childColumns = "id",
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE,
+ deferred = true
+ )}""".trimIndent()
+ )
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { entity, _ ->
+ assertThat(entity.indices, `is`(emptyList()))
+ }.compilesWithoutWarnings()
+ }
+
+ @Test
+ fun recursion_1Level() {
+ singleEntity(
+ """
+ @Embedded
+ MyEntity myEntity;
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyEntity -> foo.bar.MyEntity"))
+ }
+
+ @Test
+ fun recursion_2Levels_embedToRelation() {
+ singleEntity(
+ """
+ int pojoId;
+ @Embedded
+ A a;
+
+ static class A {
+ int entityId;
+ @Relation(parentColumn = "pojoId", entityColumn = "entityId")
+ MyEntity myEntity;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
+ }
+
+ @Test
+ fun recursion_2Levels_onlyEmbeds_entityToPojo() {
+ singleEntity(
+ """
+ @Embedded
+ A a;
+
+ static class A {
+ @Embedded
+ MyEntity myEntity;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
+ }
+
+ @Test
+ fun recursion_2Levels_onlyEmbeds_onlyEntities() {
+ singleEntity(
+ """
+ @Embedded
+ A a;
+
+ @Entity
+ static class A {
+ @Embedded
+ MyEntity myEntity;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyEntity -> foo.bar.MyEntity.A -> foo.bar.MyEntity"))
+ }
+ fun okTableName() {
+ val annotation = mapOf("tableName" to "\"foo bar\"")
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun badTableName() {
+ val annotation = mapOf("tableName" to """ "foo`bar" """)
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ String name;
+ """,
+ attributes = annotation, jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_TABLE_NAME)
+ }
+
+ @Test
+ fun badColumnName() {
+ singleEntity(
+ """
+ @PrimaryKey
+ int id;
+ @ColumnInfo(name = "\"foo bar\"")
+ String name;
+ """,
+ jfos = listOf(COMMON.USER)
+ ) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_COLUMN_NAME)
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
new file mode 100644
index 0000000..f539de6
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import androidx.room.Entity
+import androidx.room.parser.Collate
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.solver.types.ColumnTypeAdapter
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.Field
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import simpleRun
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@Suppress("HasPlatformType")
+@RunWith(JUnit4::class)
+class FieldProcessorTest {
+ companion object {
+ const val ENTITY_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ abstract class MyEntity {
+ """
+ const val ENTITY_SUFFIX = "}"
+ val ALL_PRIMITIVES = arrayListOf(
+ TypeKind.INT,
+ TypeKind.BYTE,
+ TypeKind.SHORT,
+ TypeKind.LONG,
+ TypeKind.CHAR,
+ TypeKind.FLOAT,
+ TypeKind.DOUBLE)
+ val ARRAY_CONVERTER = JavaFileObjects.forSourceLines("foo.bar.MyConverter",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ public class MyConverter {
+ ${ALL_PRIMITIVES.joinToString("\n") {
+ val arrayDef = "${it.name.toLowerCase()}[]"
+ "@TypeConverter public static String" +
+ " arrayIntoString($arrayDef input) { return null;}" +
+ "@TypeConverter public static $arrayDef" +
+ " stringIntoArray${it.name}(String input) { return null;}"
+ }}
+ ${ALL_PRIMITIVES.joinToString("\n") {
+ val arrayDef = "${it.box()}[]"
+ "@TypeConverter public static String" +
+ " arrayIntoString($arrayDef input) { return null;}" +
+ "@TypeConverter public static $arrayDef" +
+ " stringIntoArray${it.name}Boxed(String input) { return null;}"
+ }}
+ }
+ """)
+
+ private fun TypeKind.box(): String {
+ return "java.lang." + when (this) {
+ TypeKind.INT -> "Integer"
+ TypeKind.CHAR -> "Character"
+ else -> this.name.toLowerCase().capitalize()
+ }
+ }
+
+ // these 2 box methods are ugly but makes tests nicer and they are private
+ private fun TypeKind.typeMirror(invocation: TestInvocation): TypeMirror {
+ return invocation.processingEnv.typeUtils.getPrimitiveType(this)
+ }
+
+ private fun TypeKind.affinity(): SQLTypeAffinity {
+ return when (this) {
+ TypeKind.FLOAT, TypeKind.DOUBLE -> SQLTypeAffinity.REAL
+ else -> SQLTypeAffinity.INTEGER
+ }
+ }
+
+ private fun TypeKind.box(invocation: TestInvocation): TypeMirror {
+ return invocation.processingEnv.elementUtils.getTypeElement(box()).asType()
+ }
+ }
+
+ @Test
+ fun primitives() {
+ ALL_PRIMITIVES.forEach { primitive ->
+ singleEntity("${primitive.name.toLowerCase()} x;") { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "x",
+ type = primitive.typeMirror(invocation),
+ element = field.element,
+ affinity = primitive.affinity()
+ )))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun boxed() {
+ ALL_PRIMITIVES.forEach { primitive ->
+ singleEntity("${primitive.box()} y;") { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "y",
+ type = primitive.box(invocation),
+ element = field.element,
+ affinity = primitive.affinity())))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun columnName() {
+ singleEntity("""
+ @ColumnInfo(name = "foo")
+ @PrimaryKey
+ int x;
+ """) { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "x",
+ type = TypeKind.INT.typeMirror(invocation),
+ element = field.element,
+ columnName = "foo",
+ affinity = SQLTypeAffinity.INTEGER)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun indexed() {
+ singleEntity("""
+ @ColumnInfo(name = "foo", index = true)
+ int x;
+ """) { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "x",
+ type = TypeKind.INT.typeMirror(invocation),
+ element = field.element,
+ columnName = "foo",
+ affinity = SQLTypeAffinity.INTEGER,
+ indexed = true)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun emptyColumnName() {
+ singleEntity("""
+ @ColumnInfo(name = "")
+ int x;
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY)
+ }
+
+ @Test
+ fun byteArrayWithEnforcedType() {
+ singleEntity("@TypeConverters(foo.bar.MyConverter.class)" +
+ "@ColumnInfo(typeAffinity = ColumnInfo.TEXT) byte[] arr;") { field, invocation ->
+ assertThat(field, `is`(Field(name = "arr",
+ type = invocation.processingEnv.typeUtils.getArrayType(
+ TypeKind.BYTE.typeMirror(invocation)),
+ element = field.element,
+ affinity = SQLTypeAffinity.TEXT)))
+ assertThat((field.cursorValueReader as? ColumnTypeAdapter)?.typeAffinity,
+ `is`(SQLTypeAffinity.TEXT))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun primitiveArray() {
+ ALL_PRIMITIVES.forEach { primitive ->
+ singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
+ "${primitive.toString().toLowerCase()}[] arr;") { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "arr",
+ type = invocation.processingEnv.typeUtils.getArrayType(
+ primitive.typeMirror(invocation)),
+ element = field.element,
+ affinity = if (primitive == TypeKind.BYTE) {
+ SQLTypeAffinity.BLOB
+ } else {
+ SQLTypeAffinity.TEXT
+ })))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun boxedArray() {
+ ALL_PRIMITIVES.forEach { primitive ->
+ singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
+ "${primitive.box()}[] arr;") { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "arr",
+ type = invocation.processingEnv.typeUtils.getArrayType(
+ primitive.box(invocation)),
+ element = field.element,
+ affinity = SQLTypeAffinity.TEXT)))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun generic() {
+ singleEntity("""
+ static class BaseClass<T> {
+ T item;
+ }
+ @Entity
+ static class Extending extends BaseClass<java.lang.Integer> {
+ }
+ """) { field, invocation ->
+ assertThat(field, `is`(Field(name = "item",
+ type = TypeKind.INT.box(invocation),
+ element = field.element,
+ affinity = SQLTypeAffinity.INTEGER)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun unboundGeneric() {
+ singleEntity("""
+ @Entity
+ static class BaseClass<T> {
+ T item;
+ }
+ """) { _, _ -> }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
+ }
+
+ @Test
+ fun nameVariations() {
+ simpleRun {
+ assertThat(Field(mock(Element::class.java), "x", TypeKind.INT.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
+ assertThat(Field(mock(Element::class.java), "x", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
+ assertThat(Field(mock(Element::class.java), "xAll",
+ TypeKind.BOOLEAN.typeMirror(it), SQLTypeAffinity.INTEGER)
+ .nameWithVariations, `is`(arrayListOf("xAll")))
+ }
+ }
+
+ @Test
+ fun nameVariations_is() {
+ val elm = mock(Element::class.java)
+ simpleRun {
+ assertThat(Field(elm, "isX", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX", "x")))
+ assertThat(Field(elm, "isX", TypeKind.INT.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX")))
+ assertThat(Field(elm, "is", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("is")))
+ assertThat(Field(elm, "isAllItems", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations,
+ `is`(arrayListOf("isAllItems", "allItems")))
+ }
+ }
+
+ @Test
+ fun nameVariations_has() {
+ val elm = mock(Element::class.java)
+ simpleRun {
+ assertThat(Field(elm, "hasX", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX", "x")))
+ assertThat(Field(elm, "hasX", TypeKind.INT.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX")))
+ assertThat(Field(elm, "has", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("has")))
+ assertThat(Field(elm, "hasAllItems", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations,
+ `is`(arrayListOf("hasAllItems", "allItems")))
+ }
+ }
+
+ @Test
+ fun nameVariations_m() {
+ val elm = mock(Element::class.java)
+ simpleRun {
+ assertThat(Field(elm, "mall", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mall")))
+ assertThat(Field(elm, "mallVars", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mallVars")))
+ assertThat(Field(elm, "mAll", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mAll", "all")))
+ assertThat(Field(elm, "m", TypeKind.INT.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("m")))
+ assertThat(Field(elm, "mallItems", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations,
+ `is`(arrayListOf("mallItems")))
+ assertThat(Field(elm, "mAllItems", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations,
+ `is`(arrayListOf("mAllItems", "allItems")))
+ }
+ }
+
+ @Test
+ fun nameVariations_underscore() {
+ val elm = mock(Element::class.java)
+ simpleRun {
+ assertThat(Field(elm, "_all", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_all", "all")))
+ assertThat(Field(elm, "_", TypeKind.INT.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_")))
+ assertThat(Field(elm, "_allItems", TypeKind.BOOLEAN.typeMirror(it),
+ SQLTypeAffinity.INTEGER).nameWithVariations,
+ `is`(arrayListOf("_allItems", "allItems")))
+ }
+ }
+
+ @Test
+ fun collate() {
+ Collate.values().forEach { collate ->
+ singleEntity("""
+ @PrimaryKey
+ @ColumnInfo(collate = ColumnInfo.${collate.name})
+ String code;
+ """) { field, invocation ->
+ assertThat(field, `is`(
+ Field(name = "code",
+ type = invocation.context.COMMON_TYPES.STRING,
+ element = field.element,
+ columnName = "code",
+ collate = collate,
+ affinity = SQLTypeAffinity.TEXT)))
+ }.compilesWithoutError()
+ }
+ }
+
+ fun singleEntity(vararg input: String, handler: (Field, invocation: TestInvocation) -> Unit):
+ CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
+ ENTITY_PREFIX + input.joinToString("\n") + ENTITY_SUFFIX
+ ), ARRAY_CONVERTER))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(androidx.room.Entity::class)
+ .nextRunHandler { invocation ->
+ val (owner, field) = invocation.roundEnv
+ .getElementsAnnotatedWith(Entity::class.java)
+ .map {
+ Pair(it, invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .firstOrNull { it.kind == ElementKind.FIELD })
+ }
+ .first { it.second != null }
+ val entityContext =
+ EntityProcessor(invocation.context, MoreElements.asType(owner))
+ .context
+ val parser = FieldProcessor(
+ baseContext = entityContext,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ element = field!!,
+ bindingScope = FieldProcessor.BindingScope.TWO_WAY,
+ fieldParent = null)
+ handler(parser.process(), invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
new file mode 100644
index 0000000..c533ed0
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import COMMON
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.typeName
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.InsertionMethod
+import androidx.room.vo.InsertionMethod.Type
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth.assertAbout
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class InsertionMethodProcessorTest {
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ import java.util.*;
+ @Dao
+ abstract class MyClass {
+ """
+ const val DAO_SUFFIX = "}"
+ val USER_TYPE_NAME: TypeName = COMMON.USER_TYPE_NAME
+ val BOOK_TYPE_NAME: TypeName = ClassName.get("foo.bar", "Book")
+ }
+
+ @Test
+ fun readNoParams() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void foo();
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("foo"))
+ assertThat(insertion.parameters.size, `is`(0))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ assertThat(insertion.entities.size, `is`(0))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT)
+ }
+
+ @Test
+ fun insertSingle() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public long foo(User user);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("foo"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(insertion.entities["user"]?.typeName,
+ `is`(ClassName.get("foo.bar", "User") as TypeName))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.LONG))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertNotAnEntity() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void foo(NotAnEntity notValid);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("foo"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.entityType, `is`(nullValue()))
+ assertThat(insertion.entities.size, `is`(0))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
+ )
+ }
+
+ @Test
+ fun insertTwo() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void foo(User u1, User u2);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("foo"))
+
+ assertThat(insertion.parameters.size, `is`(2))
+ insertion.parameters.forEach {
+ assertThat(it.type.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(it.entityType?.typeName(), `is`(USER_TYPE_NAME))
+ }
+ assertThat(insertion.entities.size, `is`(2))
+ assertThat(insertion.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.entities["u2"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.parameters.map { it.name }, `is`(listOf("u1", "u2")))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertList() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public List<Long> insertUsers(List<User> users);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("insertUsers"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(
+ ClassName.get("java.util", "List"), USER_TYPE_NAME) as TypeName))
+ assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(insertion.entities.size, `is`(1))
+ assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.returnType.typeName(), `is`(
+ ParameterizedTypeName.get(ClassName.get("java.util", "List"),
+ ClassName.get("java.lang", "Long")) as TypeName
+ ))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertArray() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void insertUsers(User[] users);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("insertUsers"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ArrayTypeName.of(COMMON.USER_TYPE_NAME) as TypeName))
+ assertThat(insertion.entities.size, `is`(1))
+ assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertSet() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void insertUsers(Set<User> users);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("insertUsers"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(ClassName.get("java.util", "Set")
+ , COMMON.USER_TYPE_NAME) as TypeName))
+ assertThat(insertion.entities.size, `is`(1))
+ assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertQueue() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void insertUsers(Queue<User> users);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("insertUsers"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(ClassName.get("java.util", "Queue")
+ , USER_TYPE_NAME) as TypeName))
+ assertThat(insertion.entities.size, `is`(1))
+ assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertIterable() {
+ singleInsertMethod("""
+ @Insert
+ abstract public void insert(Iterable<User> users);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("insert"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(ParameterizedTypeName.get(
+ ClassName.get("java.lang", "Iterable"), USER_TYPE_NAME) as TypeName))
+ assertThat(insertion.entities.size, `is`(1))
+ assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertCustomCollection() {
+ singleInsertMethod("""
+ static class MyList<Irrelevant, Item> extends ArrayList<Item> {}
+ @Insert
+ abstract public void insert(MyList<String, User> users);
+ """) { insertion, _ ->
+ assertThat(insertion.name, `is`("insert"))
+ assertThat(insertion.parameters.size, `is`(1))
+ val param = insertion.parameters.first()
+ assertThat(param.type.typeName(), `is`(ParameterizedTypeName.get(
+ ClassName.get("foo.bar", "MyClass.MyList"),
+ CommonTypeNames.STRING, USER_TYPE_NAME) as TypeName))
+ assertThat(insertion.entities.size, `is`(1))
+ assertThat(insertion.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun insertDifferentTypes() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void foo(User u1, Book b1);
+ """) { insertion, _ ->
+ assertThat(insertion.parameters.size, `is`(2))
+ assertThat(insertion.parameters[0].type.typeName().toString(),
+ `is`("foo.bar.User"))
+ assertThat(insertion.parameters[1].type.typeName().toString(),
+ `is`("foo.bar.Book"))
+ assertThat(insertion.parameters.map { it.name }, `is`(listOf("u1", "b1")))
+ assertThat(insertion.returnType.typeName(), `is`(TypeName.VOID))
+ assertThat(insertion.entities.size, `is`(2))
+ assertThat(insertion.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(insertion.entities["b1"]?.typeName, `is`(BOOK_TYPE_NAME))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun onConflict_Default() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public void foo(User user);
+ """) { insertion, _ ->
+ assertThat(insertion.onConflict, `is`(OnConflictStrategy.ABORT))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun onConflict_Invalid() {
+ singleInsertMethod(
+ """
+ @Insert(onConflict = -1)
+ abstract public void foo(User user);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+ }
+
+ @Test
+ fun onConflict_EachValue() {
+ listOf(
+ Pair("REPLACE", 1),
+ Pair("ROLLBACK", 2),
+ Pair("ABORT", 3),
+ Pair("FAIL", 4),
+ Pair("IGNORE", 5)
+ ).forEach { pair ->
+ singleInsertMethod(
+ """
+ @Insert(onConflict=OnConflictStrategy.${pair.first})
+ abstract public void foo(User user);
+ """) { insertion, _ ->
+ assertThat(insertion.onConflict, `is`(pair.second))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun invalidReturnType() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public int foo(User user);
+ """) { insertion, _ ->
+ assertThat(insertion.insertionType, `is`(nullValue()))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.INVALID_INSERTION_METHOD_RETURN_TYPE)
+ }
+
+ @Test
+ fun mismatchedReturnType() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public long[] foo(User user);
+ """) { insertion, _ ->
+ assertThat(insertion.insertionType, `is`(nullValue()))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.insertionMethodReturnTypeMismatch(
+ ArrayTypeName.of(TypeName.LONG),
+ InsertionMethodProcessor.SINGLE_ITEM_SET.map { it.returnTypeName }))
+ }
+
+ @Test
+ fun mismatchedReturnType2() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public long foo(User... user);
+ """) { insertion, _ ->
+ assertThat(insertion.insertionType, `is`(nullValue()))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.insertionMethodReturnTypeMismatch(
+ TypeName.LONG,
+ InsertionMethodProcessor.MULTIPLE_ITEM_SET.map { it.returnTypeName }))
+ }
+
+ @Test
+ fun mismatchedReturnType3() {
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public long foo(User user1, User user2);
+ """) { insertion, _ ->
+ assertThat(insertion.insertionType, `is`(nullValue()))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.insertionMethodReturnTypeMismatch(
+ TypeName.LONG,
+ InsertionMethodProcessor.VOID_SET.map { it.returnTypeName }))
+ }
+
+ @Test
+ fun validReturnTypes() {
+ listOf(
+ Pair("void", Type.INSERT_VOID),
+ Pair("long", Type.INSERT_SINGLE_ID),
+ Pair("long[]", Type.INSERT_ID_ARRAY),
+ Pair("Long[]", Type.INSERT_ID_ARRAY_BOX),
+ Pair("List<Long>", Type.INSERT_ID_LIST)
+ ).forEach { pair ->
+ val dots = if (pair.second in setOf(Type.INSERT_ID_LIST, Type.INSERT_ID_ARRAY,
+ Type.INSERT_ID_ARRAY_BOX)) {
+ "..."
+ } else {
+ ""
+ }
+ singleInsertMethod(
+ """
+ @Insert
+ abstract public ${pair.first} foo(User$dots user);
+ """) { insertion, _ ->
+ assertThat(insertion.insertMethodTypeFor(insertion.parameters.first()),
+ `is`(pair.second))
+ assertThat(pair.toString(), insertion.insertionType, `is`(pair.second))
+ }.compilesWithoutError()
+ }
+ }
+
+ fun singleInsertMethod(
+ vararg input: String,
+ handler: (InsertionMethod, TestInvocation) -> Unit
+ ): CompileTester {
+ return assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+ ), COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Insert::class, Dao::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ MoreElements.isAnnotationPresent(it,
+ Insert::class.java)
+ }
+ )
+ }.filter { it.second.isNotEmpty() }.first()
+ val processor = InsertionMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ val processed = processor.process()
+ handler(processed, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
new file mode 100644
index 0000000..3506973
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -0,0 +1,958 @@
+/*
+ * 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 androidx.room.processor
+
+import COMMON
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_TYPE
+import androidx.room.processor.ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
+import androidx.room.processor.ProcessorErrors.POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
+import androidx.room.processor.ProcessorErrors.RELATION_NOT_COLLECTION
+import androidx.room.processor.ProcessorErrors.relationCannotFindEntityField
+import androidx.room.processor.ProcessorErrors.relationCannotFindParentEntityField
+import androidx.room.testing.TestInvocation
+import androidx.room.vo.CallType
+import androidx.room.vo.Constructor
+import androidx.room.vo.EmbeddedField
+import androidx.room.vo.Field
+import androidx.room.vo.Pojo
+import androidx.room.vo.RelationCollector
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.CoreMatchers.sameInstance
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import simpleRun
+import javax.lang.model.element.Element
+import javax.tools.JavaFileObject
+
+/**
+ * Some of the functionality is tested via EntityProcessor.
+ */
+@RunWith(JUnit4::class)
+class PojoProcessorTest {
+
+ companion object {
+ val MY_POJO: ClassName = ClassName.get("foo.bar", "MyPojo")
+ val HEADER = """
+ package foo.bar;
+ import androidx.room.*;
+ import java.util.*;
+ public class MyPojo {
+ """
+ val FOOTER = "\n}"
+ }
+
+ private fun String.toJFO(qName: String) = JavaFileObjects.forSourceLines(qName, this)
+
+ @Test
+ fun inheritedPrivate() {
+ val parent = """
+ package foo.bar.x;
+ import androidx.room.*;
+ public class BaseClass {
+ private String baseField;
+ public String getBaseField(){ return baseField; }
+ public void setBaseField(String baseField){ }
+ }
+ """
+ simpleRun(
+ """
+ package foo.bar;
+ import androidx.room.*;
+ public class ${MY_POJO.simpleName()} extends foo.bar.x.BaseClass {
+ public String myField;
+ }
+ """.toJFO(MY_POJO.toString()),
+ parent.toJFO("foo.bar.x.BaseClass")) { invocation ->
+ val pojo = PojoProcessor(baseContext = invocation.context,
+ element = invocation.typeElement(MY_POJO.toString()),
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = null).process()
+ assertThat(pojo.fields.find { it.name == "myField" }, notNullValue())
+ assertThat(pojo.fields.find { it.name == "baseField" }, notNullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun transient_ignore() {
+ singleRun("""
+ transient int foo;
+ int bar;
+ """) { pojo ->
+ assertThat(pojo.fields.size, `is`(1))
+ assertThat(pojo.fields[0].name, `is`("bar"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun transient_withColumnInfo() {
+ singleRun("""
+ @ColumnInfo
+ transient int foo;
+ int bar;
+ """) { pojo ->
+ assertThat(pojo.fields.map { it.name }.toSet(), `is`(setOf("bar", "foo")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun transient_embedded() {
+ singleRun("""
+ @Embedded
+ transient Foo foo;
+ int bar;
+ static class Foo {
+ int x;
+ }
+ """) { pojo ->
+ assertThat(pojo.fields.map { it.name }.toSet(), `is`(setOf("x", "bar")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun transient_insideEmbedded() {
+ singleRun("""
+ @Embedded
+ Foo foo;
+ int bar;
+ static class Foo {
+ transient int x;
+ int y;
+ }
+ """) { pojo ->
+ assertThat(pojo.fields.map { it.name }.toSet(), `is`(setOf("bar", "y")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun transient_relation() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public transient List<User> user;
+ """, COMMON.USER
+ ) { pojo ->
+ assertThat(pojo.relations.size, `is`(1))
+ assertThat(pojo.relations.first().entityField.name, `is`("uid"))
+ assertThat(pojo.relations.first().parentField.name, `is`("id"))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun embedded() {
+ singleRun(
+ """
+ int id;
+ @Embedded
+ Point myPoint;
+ static class Point {
+ int x;
+ int y;
+ }
+ """
+ ) { pojo ->
+ assertThat(pojo.fields.size, `is`(3))
+ assertThat(pojo.fields[1].name, `is`("x"))
+ assertThat(pojo.fields[2].name, `is`("y"))
+ assertThat(pojo.fields[0].parent, nullValue())
+ assertThat(pojo.fields[1].parent, notNullValue())
+ assertThat(pojo.fields[2].parent, notNullValue())
+ val parent = pojo.fields[2].parent!!
+ assertThat(parent.prefix, `is`(""))
+ assertThat(parent.field.name, `is`("myPoint"))
+ assertThat(parent.pojo.typeName,
+ `is`(ClassName.get("foo.bar.MyPojo", "Point") as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun embeddedWithPrefix() {
+ singleRun(
+ """
+ int id;
+ @Embedded(prefix = "foo")
+ Point myPoint;
+ static class Point {
+ int x;
+ @ColumnInfo(name = "y2")
+ int y;
+ }
+ """
+ ) { pojo ->
+ assertThat(pojo.fields.size, `is`(3))
+ assertThat(pojo.fields[1].name, `is`("x"))
+ assertThat(pojo.fields[2].name, `is`("y"))
+ assertThat(pojo.fields[1].columnName, `is`("foox"))
+ assertThat(pojo.fields[2].columnName, `is`("fooy2"))
+ val parent = pojo.fields[2].parent!!
+ assertThat(parent.prefix, `is`("foo"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun nestedEmbedded() {
+ singleRun(
+ """
+ int id;
+ @Embedded(prefix = "foo")
+ Point myPoint;
+ static class Point {
+ int x;
+ @ColumnInfo(name = "y2")
+ int y;
+ @Embedded(prefix = "bar")
+ Coordinate coordinate;
+ }
+ static class Coordinate {
+ double lat;
+ double lng;
+ @Ignore
+ String ignored;
+ }
+ """
+ ) { pojo ->
+ assertThat(pojo.fields.size, `is`(5))
+ assertThat(pojo.fields.map { it.columnName }, `is`(
+ listOf("id", "foox", "fooy2", "foobarlat", "foobarlng")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun duplicateColumnNames() {
+ singleRun(
+ """
+ int id;
+ @ColumnInfo(name = "id")
+ int another;
+ """
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.pojoDuplicateFieldNames("id", listOf("id", "another"))
+ ).and().withErrorContaining(
+ POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
+ ).and().withErrorCount(3)
+ }
+
+ @Test
+ fun duplicateColumnNamesFromEmbedded() {
+ singleRun(
+ """
+ int id;
+ @Embedded
+ Foo foo;
+ static class Foo {
+ @ColumnInfo(name = "id")
+ int x;
+ }
+ """
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.pojoDuplicateFieldNames("id", listOf("id", "foo > x"))
+ ).and().withErrorContaining(
+ POJO_FIELD_HAS_DUPLICATE_COLUMN_NAME
+ ).and().withErrorCount(3)
+ }
+
+ @Test
+ fun dropSubPrimaryKeyNoWarningForPojo() {
+ singleRun(
+ """
+ @PrimaryKey
+ int id;
+ @Embedded
+ Point myPoint;
+ static class Point {
+ @PrimaryKey
+ int x;
+ int y;
+ }
+ """
+ ) { _ ->
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun relation_notCollection() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public User user;
+ """, COMMON.USER
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(RELATION_NOT_COLLECTION)
+ }
+
+ @Test
+ fun relation_columnInfo() {
+ singleRun(
+ """
+ int id;
+ @ColumnInfo
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public List<User> user;
+ """, COMMON.USER
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.CANNOT_USE_MORE_THAN_ONE_POJO_FIELD_ANNOTATION)
+ }
+
+ @Test
+ fun relation_notEntity() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public List<NotAnEntity> user;
+ """, COMMON.NOT_AN_ENTITY
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY)
+ }
+
+ @Test
+ fun relation_missingParent() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "idk", entityColumn = "uid")
+ public List<User> user;
+ """, COMMON.USER
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(
+ relationCannotFindParentEntityField("foo.bar.MyPojo", "idk", listOf("id"))
+ )
+ }
+
+ @Test
+ fun relation_missingEntityField() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "idk")
+ public List<User> user;
+ """, COMMON.USER
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(
+ relationCannotFindEntityField("foo.bar.User", "idk",
+ listOf("uid", "name", "lastName", "age"))
+ )
+ }
+
+ @Test
+ fun relation_missingType() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public List<User> user;
+ """
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(CANNOT_FIND_TYPE)
+ }
+
+ @Test
+ fun relation_nestedField() {
+ singleRun(
+ """
+ static class Nested {
+ @ColumnInfo(name = "foo")
+ public int id;
+ }
+ @Embedded
+ Nested nested;
+ @Relation(parentColumn = "foo", entityColumn = "uid")
+ public List<User> user;
+ """, COMMON.USER
+ ) { pojo ->
+ assertThat(pojo.relations.first().parentField.columnName, `is`("foo"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun relation_nestedRelation() {
+ singleRun(
+ """
+ static class UserWithNested {
+ @Embedded
+ public User user;
+ @Relation(parentColumn = "uid", entityColumn = "uid")
+ public List<User> selfs;
+ }
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid", entity = User.class)
+ public List<UserWithNested> user;
+ """, COMMON.USER
+ ) { pojo, _ ->
+ assertThat(pojo.relations.first().parentField.name, `is`("id"))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun relation_affinityMismatch() {
+ singleRun(
+ """
+ String id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public List<User> user;
+ """, COMMON.USER
+ ) { pojo, invocation ->
+ // trigger assignment evaluation
+ RelationCollector.createCollectors(invocation.context, pojo.relations)
+ assertThat(pojo.relations.size, `is`(1))
+ assertThat(pojo.relations.first().entityField.name, `is`("uid"))
+ assertThat(pojo.relations.first().parentField.name, `is`("id"))
+ }.compilesWithoutError().withWarningContaining(
+ ProcessorErrors.relationAffinityMismatch(
+ parentAffinity = SQLTypeAffinity.TEXT,
+ childAffinity = SQLTypeAffinity.INTEGER,
+ parentColumn = "id",
+ childColumn = "uid")
+ )
+ }
+
+ @Test
+ fun relation_simple() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ public List<User> user;
+ """, COMMON.USER
+ ) { pojo ->
+ assertThat(pojo.relations.size, `is`(1))
+ assertThat(pojo.relations.first().entityField.name, `is`("uid"))
+ assertThat(pojo.relations.first().parentField.name, `is`("id"))
+ }.compilesWithoutError().withWarningCount(0)
+ }
+
+ @Test
+ fun relation_badProjection() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid", projection={"i_dont_exist"})
+ public List<User> user;
+ """, COMMON.USER
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.relationBadProject("foo.bar.User", listOf("i_dont_exist"),
+ listOf("uid", "name", "lastName", "ageColumn"))
+ )
+ }
+
+ @Test
+ fun relation_badReturnTypeInGetter() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid")
+ private List<User> user;
+ public void setUser(List<User> user){ this.user = user;}
+ public User getUser(){return null;}
+ """, COMMON.USER
+ ) { _ ->
+ }.failsToCompile().withErrorContaining(CANNOT_FIND_GETTER_FOR_FIELD)
+ }
+
+ @Test
+ fun relation_primitiveList() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid", projection={"uid"},
+ entity = User.class)
+ public List<Integer> userIds;
+ """, COMMON.USER
+ ) { pojo ->
+ assertThat(pojo.relations.size, `is`(1))
+ val rel = pojo.relations.first()
+ assertThat(rel.projection, `is`(listOf("uid")))
+ assertThat(rel.entity.typeName, `is`(COMMON.USER_TYPE_NAME as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun relation_stringList() {
+ singleRun(
+ """
+ int id;
+ @Relation(parentColumn = "id", entityColumn = "uid", projection={"name"},
+ entity = User.class)
+ public List<String> userNames;
+ """, COMMON.USER
+ ) { pojo ->
+ assertThat(pojo.relations.size, `is`(1))
+ val rel = pojo.relations.first()
+ assertThat(rel.projection, `is`(listOf("name")))
+ assertThat(rel.entity.typeName, `is`(COMMON.USER_TYPE_NAME as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun cache() {
+ val pojo = """
+ $HEADER
+ int id;
+ $FOOTER
+ """.toJFO(MY_POJO.toString())
+ simpleRun(pojo) { invocation ->
+ val element = invocation.typeElement(MY_POJO.toString())
+ val pojo1 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.BIND_TO_STMT, null).process()
+ assertThat(pojo1, notNullValue())
+ val pojo2 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.BIND_TO_STMT, null).process()
+ assertThat(pojo2, sameInstance(pojo1))
+
+ val pojo3 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.READ_FROM_CURSOR, null).process()
+ assertThat(pojo3, notNullValue())
+ assertThat(pojo3, not(sameInstance(pojo1)))
+
+ val pojo4 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.TWO_WAY, null).process()
+ assertThat(pojo4, notNullValue())
+ assertThat(pojo4, not(sameInstance(pojo1)))
+ assertThat(pojo4, not(sameInstance(pojo3)))
+
+ val pojo5 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.TWO_WAY, null).process()
+ assertThat(pojo5, sameInstance(pojo4))
+
+ val type = invocation.context.COMMON_TYPES.STRING
+ val mockElement = mock(Element::class.java)
+ doReturn(type).`when`(mockElement).asType()
+ val fakeField = Field(
+ element = mockElement,
+ name = "foo",
+ type = type,
+ affinity = SQLTypeAffinity.TEXT,
+ columnName = "foo",
+ parent = null,
+ indexed = false
+ )
+ val fakeEmbedded = EmbeddedField(fakeField, "", null)
+
+ val pojo6 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.TWO_WAY, fakeEmbedded).process()
+ assertThat(pojo6, notNullValue())
+ assertThat(pojo6, not(sameInstance(pojo1)))
+ assertThat(pojo6, not(sameInstance(pojo3)))
+ assertThat(pojo6, not(sameInstance(pojo4)))
+
+ val pojo7 = PojoProcessor(invocation.context, element,
+ FieldProcessor.BindingScope.TWO_WAY, fakeEmbedded).process()
+ assertThat(pojo7, sameInstance(pojo6))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_empty() {
+ val pojoCode = """
+ public String mName;
+ """
+ singleRun(pojoCode) { pojo ->
+ assertThat(pojo.constructor, notNullValue())
+ assertThat(pojo.constructor?.params, `is`(emptyList<Constructor.Param>()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_ambiguous_twoFieldsExactMatch() {
+ val pojoCode = """
+ public String mName;
+ public String _name;
+ public MyPojo(String mName) {
+ }
+ """
+ singleRun(pojoCode) { pojo ->
+ val param = pojo.constructor?.params?.first()
+ assertThat(param, instanceOf(Constructor.FieldParam::class.java))
+ assertThat((param as Constructor.FieldParam).field.name, `is`("mName"))
+ assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_ambiguous_oneTypeMatches() {
+ val pojoCode = """
+ public String mName;
+ public int _name;
+ public MyPojo(String name) {
+ }
+ """
+ singleRun(pojoCode) { pojo ->
+ val param = pojo.constructor?.params?.first()
+ assertThat(param, instanceOf(Constructor.FieldParam::class.java))
+ assertThat((param as Constructor.FieldParam).field.name, `is`("mName"))
+ assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_ambiguous_twoFields() {
+ val pojo = """
+ String mName;
+ String _name;
+ public MyPojo(String name) {
+ }
+ """
+ singleRun(pojo) { _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.ambigiousConstructor(MY_POJO.toString(),
+ "name", listOf("mName", "_name"))
+ )
+ }
+
+ @Test
+ fun constructor_noMatchBadType() {
+ singleRun("""
+ int foo;
+ public MyPojo(String foo) {
+ }
+ """) { _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ }
+
+ @Test
+ fun constructor_noMatch() {
+ singleRun("""
+ String mName;
+ String _name;
+ public MyPojo(String foo) {
+ }
+ """) { _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ }
+
+ @Test
+ fun constructor_noMatchMultiArg() {
+ singleRun("""
+ String mName;
+ int bar;
+ public MyPojo(String foo, String name) {
+ }
+ """) { _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_POJO_CONSTRUCTOR)
+ }
+
+ @Test
+ fun constructor_multipleMatching() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ public MyPojo(String name) {
+ }
+ public MyPojo(String name, String lastName) {
+ }
+ """) { _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS)
+ }
+
+ @Test
+ fun constructor_multipleMatchingWithIgnored() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ @Ignore
+ public MyPojo(String name) {
+ }
+ public MyPojo(String name, String lastName) {
+ }
+ """) { pojo ->
+ assertThat(pojo.constructor, notNullValue())
+ assertThat(pojo.constructor?.params?.size, `is`(2))
+ assertThat(pojo.fields.find { it.name == "mName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ assertThat(pojo.fields.find { it.name == "mLastName" }?.setter?.callType,
+ `is`(CallType.CONSTRUCTOR))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_dontTryForBindToScope() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ """) { _, invocation ->
+ val process2 = PojoProcessor(baseContext = invocation.context,
+ element = invocation.typeElement(MY_POJO.toString()),
+ bindingScope = FieldProcessor.BindingScope.BIND_TO_STMT,
+ parent = null).process()
+ assertThat(process2.constructor, nullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_bindForTwoWay() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ """) { _, invocation ->
+ val process2 = PojoProcessor(baseContext = invocation.context,
+ element = invocation.typeElement(MY_POJO.toString()),
+ bindingScope = FieldProcessor.BindingScope.TWO_WAY,
+ parent = null).process()
+ assertThat(process2.constructor, notNullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun constructor_multipleMatching_withNoArg() {
+ singleRun("""
+ String mName;
+ String mLastName;
+ public MyPojo() {
+ }
+ public MyPojo(String name, String lastName) {
+ }
+ """) { pojo ->
+ assertThat(pojo.constructor?.params?.size ?: -1, `is`(0))
+ }.compilesWithoutError().withWarningContaining(
+ ProcessorErrors.TOO_MANY_POJO_CONSTRUCTORS_CHOOSING_NO_ARG)
+ }
+
+ @Test // added for b/69562125
+ fun constructor_withNullabilityAnnotation() {
+ singleRun("""
+ String mName;
+ public MyPojo(@androidx.annotation.NonNull String name) {}
+ """) { pojo ->
+ val constructor = pojo.constructor
+ assertThat(constructor, notNullValue())
+ assertThat(constructor!!.params.size, `is`(1))
+ }.compilesWithoutError()
+ }
+
+ @Test // b/69118713 common mistake so we better provide a good explanation
+ fun constructor_relationParameter() {
+ singleRun("""
+ @Relation(entity = foo.bar.User.class, parentColumn = "uid", entityColumn="uid",
+ projection = "name")
+ public List<String> items;
+ public String uid;
+ public MyPojo(String uid, List<String> items) {
+ }
+ """, COMMON.USER) { _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RELATION_CANNOT_BE_CONSTRUCTOR_PARAMETER
+ )
+ }
+
+ @Test
+ fun recursion_1Level() {
+ singleRun(
+ """
+ @Embedded
+ MyPojo myPojo;
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo"))
+ }
+
+ @Test
+ fun recursion_2Levels_relationToEmbed() {
+ singleRun(
+ """
+ int pojoId;
+
+ @Relation(parentColumn = "pojoId", entityColumn = "entityId")
+ List<MyEntity> myEntity;
+
+ @Entity
+ static class MyEntity {
+ int entityId;
+
+ @Embedded
+ MyPojo myPojo;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo.MyEntity -> foo.bar.MyPojo"))
+ }
+
+ @Test
+ fun recursion_2Levels_onlyEmbeds_pojoToEntity() {
+ singleRun(
+ """
+ @Embedded
+ A a;
+
+ @Entity
+ static class A {
+ @Embedded
+ MyPojo myPojo;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
+ }
+
+ @Test
+ fun recursion_2Levels_onlyEmbeds_onlyPojos() {
+ singleRun(
+ """
+ @Embedded
+ A a;
+ static class A {
+ @Embedded
+ MyPojo myPojo;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
+ }
+
+ @Test
+ fun recursion_3Levels() {
+ singleRun(
+ """
+ @Embedded
+ A a;
+ public static class A {
+ @Embedded
+ B b;
+ }
+ public static class B {
+ @Embedded
+ MyPojo myPojo;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo"))
+ }
+
+ @Test
+ fun recursion_1Level_1LevelDown() {
+ singleRun(
+ """
+ @Embedded
+ A a;
+ static class A {
+ @Embedded
+ B b;
+ }
+ static class B {
+ @Embedded
+ A a;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo.A -> foo.bar.MyPojo.B -> foo.bar.MyPojo.A"))
+ }
+
+ @Test
+ fun recursion_branchAtLevel0_afterBackTrack() {
+ singleRun(
+ """
+ @PrimaryKey
+ int id;
+ @Embedded
+ A a;
+ @Embedded
+ C c;
+ static class A {
+ @Embedded
+ B b;
+ }
+ static class B {
+ }
+ static class C {
+ @Embedded
+ MyPojo myPojo;
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo.C -> foo.bar.MyPojo"))
+ }
+
+ @Test
+ fun recursion_branchAtLevel1_afterBackTrack() {
+ singleRun(
+ """
+ @PrimaryKey
+ int id;
+ @Embedded
+ A a;
+ static class A {
+ @Embedded
+ B b;
+ @Embedded
+ MyPojo myPojo;
+ }
+ static class B {
+ @Embedded
+ C c;
+ }
+ static class C {
+ }
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RECURSIVE_REFERENCE_DETECTED.format(
+ "foo.bar.MyPojo -> foo.bar.MyPojo.A -> foo.bar.MyPojo"))
+ }
+
+ fun singleRun(
+ code: String, vararg jfos: JavaFileObject, handler: (Pojo) -> Unit): CompileTester {
+ return singleRun(code, *jfos) { pojo, _ ->
+ handler(pojo)
+ }
+ }
+
+ fun singleRun(
+ code: String, vararg jfos: JavaFileObject,
+ handler: (Pojo, TestInvocation) -> Unit): CompileTester {
+ val pojoJFO = """
+ $HEADER
+ $code
+ $FOOTER
+ """.toJFO(MY_POJO.toString())
+ val all = (jfos.toList() + pojoJFO).toTypedArray()
+ return simpleRun(*all) { invocation ->
+ handler.invoke(
+ PojoProcessor(baseContext = invocation.context,
+ element = invocation.typeElement(MY_POJO.toString()),
+ bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
+ parent = null).process(),
+ invocation
+ )
+ }
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
new file mode 100644
index 0000000..9d25c53
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -0,0 +1,817 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.processor
+
+import COMMON
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.typeName
+import androidx.room.parser.Table
+import androidx.room.processor.ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER
+import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder
+import androidx.room.solver.query.result.LiveDataQueryResultBinder
+import androidx.room.solver.query.result.PojoRowAdapter
+import androidx.room.solver.query.result.SingleEntityQueryResultAdapter
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.Field
+import androidx.room.vo.QueryMethod
+import androidx.room.vo.Warning
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth.assertAbout
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeVariableName
+import createVerifierFromEntities
+import mockElementAndType
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.hasItem
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind.INT
+import javax.lang.model.type.TypeMirror
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(Parameterized::class)
+class QueryMethodProcessorTest(val enableVerification: Boolean) {
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+ @Dao
+ abstract class MyClass {
+ """
+ const val DAO_SUFFIX = "}"
+ val POJO: ClassName = ClassName.get("foo.bar", "MyClass.Pojo")
+ @Parameterized.Parameters(name = "enableDbVerification={0}")
+ @JvmStatic
+ fun getParams() = arrayOf(true, false)
+
+ fun createField(name: String, columnName: String? = null): Field {
+ val (element, type) = mockElementAndType()
+ return Field(
+ element = element,
+ name = name,
+ type = type,
+ columnName = columnName ?: name,
+ affinity = null
+ )
+ }
+ }
+
+ @Test
+ fun testReadNoParams() {
+ singleQueryMethod(
+ """
+ @Query("SELECT * from User")
+ abstract public int[] foo();
+ """) { parsedQuery, _ ->
+ assertThat(parsedQuery.name, `is`("foo"))
+ assertThat(parsedQuery.parameters.size, `is`(0))
+ assertThat(parsedQuery.returnType.typeName(),
+ `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testSingleParam() {
+ singleQueryMethod(
+ """
+ @Query("SELECT * from User where uid = :x")
+ abstract public long foo(int x);
+ """) { parsedQuery, invocation ->
+ assertThat(parsedQuery.name, `is`("foo"))
+ assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG))
+ assertThat(parsedQuery.parameters.size, `is`(1))
+ val param = parsedQuery.parameters.first()
+ assertThat(param.name, `is`("x"))
+ assertThat(param.sqlName, `is`("x"))
+ assertThat(param.type,
+ `is`(invocation.processingEnv.typeUtils.getPrimitiveType(INT) as TypeMirror))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testVarArgs() {
+ singleQueryMethod(
+ """
+ @Query("SELECT * from User where uid in (:ids)")
+ abstract public long foo(int... ids);
+ """) { parsedQuery, invocation ->
+ assertThat(parsedQuery.name, `is`("foo"))
+ assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG))
+ assertThat(parsedQuery.parameters.size, `is`(1))
+ val param = parsedQuery.parameters.first()
+ assertThat(param.name, `is`("ids"))
+ assertThat(param.sqlName, `is`("ids"))
+ val types = invocation.processingEnv.typeUtils
+ assertThat(param.type,
+ `is`(types.getArrayType(types.getPrimitiveType(INT)) as TypeMirror))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testParamBindingMatchingNoName() {
+ singleQueryMethod(
+ """
+ @Query("SELECT uid from User where uid = :id")
+ abstract public long getIdById(int id);
+ """) { parsedQuery, _ ->
+ val section = parsedQuery.query.bindSections.first()
+ val param = parsedQuery.parameters.firstOrNull()
+ assertThat(section, notNullValue())
+ assertThat(param, notNullValue())
+ assertThat(parsedQuery.sectionToParamMapping, `is`(listOf(Pair(section, param))))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testParamBindingMatchingSimpleBind() {
+ singleQueryMethod(
+ """
+ @Query("SELECT uid from User where uid = :id")
+ abstract public long getIdById(int id);
+ """) { parsedQuery, _ ->
+ val section = parsedQuery.query.bindSections.first()
+ val param = parsedQuery.parameters.firstOrNull()
+ assertThat(section, notNullValue())
+ assertThat(param, notNullValue())
+ assertThat(parsedQuery.sectionToParamMapping,
+ `is`(listOf(Pair(section, param))))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testParamBindingTwoBindVarsIntoTheSameParameter() {
+ singleQueryMethod(
+ """
+ @Query("SELECT uid from User where uid = :id OR uid = :id")
+ abstract public long getIdById(int id);
+ """) { parsedQuery, _ ->
+ val section = parsedQuery.query.bindSections[0]
+ val section2 = parsedQuery.query.bindSections[1]
+ val param = parsedQuery.parameters.firstOrNull()
+ assertThat(section, notNullValue())
+ assertThat(section2, notNullValue())
+ assertThat(param, notNullValue())
+ assertThat(parsedQuery.sectionToParamMapping,
+ `is`(listOf(Pair(section, param), Pair(section2, param))))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testMissingParameterForBinding() {
+ singleQueryMethod(
+ """
+ @Query("SELECT uid from User where uid = :id OR uid = :uid")
+ abstract public long getIdById(int id);
+ """) { parsedQuery, _ ->
+ val section = parsedQuery.query.bindSections[0]
+ val section2 = parsedQuery.query.bindSections[1]
+ val param = parsedQuery.parameters.firstOrNull()
+ assertThat(section, notNullValue())
+ assertThat(section2, notNullValue())
+ assertThat(param, notNullValue())
+ assertThat(parsedQuery.sectionToParamMapping,
+ `is`(listOf(Pair(section, param), Pair(section2, null))))
+ }
+ .failsToCompile()
+ .withErrorContaining(
+ ProcessorErrors.missingParameterForBindVariable(listOf(":uid")))
+ }
+
+ @Test
+ fun test2MissingParameterForBinding() {
+ singleQueryMethod(
+ """
+ @Query("SELECT uid from User where name = :bar AND uid = :id OR uid = :uid")
+ abstract public long getIdById(int id);
+ """) { parsedQuery, _ ->
+ val bar = parsedQuery.query.bindSections[0]
+ val id = parsedQuery.query.bindSections[1]
+ val uid = parsedQuery.query.bindSections[2]
+ val param = parsedQuery.parameters.firstOrNull()
+ assertThat(bar, notNullValue())
+ assertThat(id, notNullValue())
+ assertThat(uid, notNullValue())
+ assertThat(param, notNullValue())
+ assertThat(parsedQuery.sectionToParamMapping,
+ `is`(listOf(Pair(bar, null), Pair(id, param), Pair(uid, null))))
+ }
+ .failsToCompile()
+ .withErrorContaining(
+ ProcessorErrors.missingParameterForBindVariable(listOf(":bar", ":uid")))
+ }
+
+ @Test
+ fun testUnusedParameters() {
+ singleQueryMethod(
+ """
+ @Query("SELECT uid from User where name = :bar")
+ abstract public long getIdById(int bar, int whyNotUseMe);
+ """) { parsedQuery, _ ->
+ val bar = parsedQuery.query.bindSections[0]
+ val barParam = parsedQuery.parameters.firstOrNull()
+ assertThat(bar, notNullValue())
+ assertThat(barParam, notNullValue())
+ assertThat(parsedQuery.sectionToParamMapping,
+ `is`(listOf(Pair(bar, barParam))))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.unusedQueryMethodParameter(listOf("whyNotUseMe")))
+ }
+
+ @Test
+ fun testNameWithUnderscore() {
+ singleQueryMethod(
+ """
+ @Query("select * from User where uid = :_blah")
+ abstract public long getSth(int _blah);
+ """
+ ) { _, _ -> }
+ .failsToCompile()
+ .withErrorContaining(ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
+ }
+
+ @Test
+ fun testGenericReturnType() {
+ singleQueryMethod(
+ """
+ @Query("select * from User")
+ abstract public <T> ${CommonTypeNames.LIST}<T> foo(int x);
+ """) { parsedQuery, _ ->
+ val expected: TypeName = ParameterizedTypeName.get(ClassName.get(List::class.java),
+ TypeVariableName.get("T"))
+ assertThat(parsedQuery.returnType.typeName(), `is`(expected))
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS)
+ }
+
+ @Test
+ fun testBadQuery() {
+ singleQueryMethod(
+ """
+ @Query("select * from :1 :2")
+ abstract public long foo(int x);
+ """) { _, _ ->
+ // do nothing
+ }.failsToCompile()
+ .withErrorContaining("UNEXPECTED_CHAR=:")
+ }
+
+ @Test
+ fun testLiveDataWithWithClause() {
+ singleQueryMethod(
+ """
+ @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
+ + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User")
+ abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
+ getFactorialLiveData();
+ """) { parsedQuery, _ ->
+ assertThat(parsedQuery.query.tables, hasItem(Table("User", "User")))
+ assertThat(parsedQuery.query.tables,
+ not(hasItem(Table("tempTable", "tempTable"))))
+ assertThat(parsedQuery.query.tables.size, `is`(1))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testLiveDataWithNothingToObserve() {
+ singleQueryMethod(
+ """
+ @Query("SELECT 1")
+ abstract public ${LifecyclesTypeNames.LIVE_DATA}<Integer> getOne();
+ """) { _, _ ->
+ // do nothing
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+
+ @Test
+ fun testLiveDataWithWithClauseAndNothingToObserve() {
+ singleQueryMethod(
+ """
+ @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
+ + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable")
+ abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
+ getFactorialLiveData();
+ """) { _, _ ->
+ // do nothing
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+
+ @Test
+ fun testBoundGeneric() {
+ singleQueryMethod(
+ """
+ static abstract class BaseModel<T> {
+ @Query("select COUNT(*) from User")
+ abstract public T getT();
+ }
+ @Dao
+ static abstract class ExtendingModel extends BaseModel<Integer> {
+ }
+ """) { parsedQuery, _ ->
+ assertThat(parsedQuery.returnType.typeName(),
+ `is`(ClassName.get(Integer::class.java) as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testBoundGenericParameter() {
+ singleQueryMethod(
+ """
+ static abstract class BaseModel<T> {
+ @Query("select COUNT(*) from User where :t")
+ abstract public int getT(T t);
+ }
+ @Dao
+ static abstract class ExtendingModel extends BaseModel<Integer> {
+ }
+ """) { parsedQuery, invocation ->
+ assertThat(parsedQuery.parameters.first().type,
+ `is`(invocation.processingEnv.elementUtils
+ .getTypeElement("java.lang.Integer").asType()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testReadDeleteWithBadReturnType() {
+ singleQueryMethod(
+ """
+ @Query("DELETE from User where uid = :id")
+ abstract public float foo(int id);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT
+ )
+ }
+
+ @Test
+ fun testSimpleDelete() {
+ singleQueryMethod(
+ """
+ @Query("DELETE from User where uid = :id")
+ abstract public int foo(int id);
+ """) { parsedQuery, _ ->
+ assertThat(parsedQuery.name, `is`("foo"))
+ assertThat(parsedQuery.parameters.size, `is`(1))
+ assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.INT))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testVoidDeleteQuery() {
+ singleQueryMethod(
+ """
+ @Query("DELETE from User where uid = :id")
+ abstract public void foo(int id);
+ """) { parsedQuery, _ ->
+ assertThat(parsedQuery.name, `is`("foo"))
+ assertThat(parsedQuery.parameters.size, `is`(1))
+ assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testVoidUpdateQuery() {
+ singleQueryMethod(
+ """
+ @Query("update user set name = :name")
+ abstract public void updateAllNames(String name);
+ """) { parsedQuery, invocation ->
+ assertThat(parsedQuery.name, `is`("updateAllNames"))
+ assertThat(parsedQuery.parameters.size, `is`(1))
+ assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID))
+ assertThat(parsedQuery.parameters.first().type.typeName(),
+ `is`(invocation.context.COMMON_TYPES.STRING.typeName()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testLiveDataQuery() {
+ singleQueryMethod(
+ """
+ @Query("select name from user where uid = :id")
+ abstract ${LifecyclesTypeNames.LIVE_DATA}<String> nameLiveData(String id);
+ """
+ ) { parsedQuery, _ ->
+ assertThat(parsedQuery.returnType.typeName(),
+ `is`(ParameterizedTypeName.get(LifecyclesTypeNames.LIVE_DATA,
+ String::class.typeName()) as TypeName))
+ assertThat(parsedQuery.queryResultBinder,
+ instanceOf(LiveDataQueryResultBinder::class.java))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testNonSelectLiveData() {
+ singleQueryMethod(
+ """
+ @Query("delete from user where uid = :id")
+ abstract ${LifecyclesTypeNames.LIVE_DATA}<Integer> deleteLiveData(String id);
+ """
+ ) { _, _ ->
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT)
+ }
+
+ @Test
+ fun testDataSourceFactoryQuery() {
+ singleQueryMethod(
+ """
+ @Query("select name from user")
+ abstract ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, String>
+ nameDataSourceFactory();
+ """
+ ) { parsedQuery, _ ->
+ assertThat(parsedQuery.returnType.typeName(),
+ `is`(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY,
+ Integer::class.typeName(), String::class.typeName()) as TypeName))
+ assertThat(parsedQuery.queryResultBinder,
+ instanceOf(DataSourceFactoryQueryResultBinder::class.java))
+ val tableNames =
+ (parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder)
+ .positionalDataSourceQueryResultBinder.tableNames
+ assertEquals(setOf("user"), tableNames)
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testMultiTableDataSourceFactoryQuery() {
+ singleQueryMethod(
+ """
+ @Query("select name from User u LEFT OUTER JOIN Book b ON u.uid == b.uid")
+ abstract ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, String>
+ nameDataSourceFactory();
+ """
+ ) { parsedQuery, _ ->
+ assertThat(parsedQuery.returnType.typeName(),
+ `is`(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY,
+ Integer::class.typeName(), String::class.typeName()) as TypeName))
+ assertThat(parsedQuery.queryResultBinder,
+ instanceOf(DataSourceFactoryQueryResultBinder::class.java))
+ val tableNames =
+ (parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder)
+ .positionalDataSourceQueryResultBinder.tableNames
+ assertEquals(setOf("User", "Book"), tableNames)
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_delete() {
+ singleQueryMethod(
+ """
+ @Query("delete from user where uid = :id")
+ abstract int deleteUser(String id);
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_update() {
+ singleQueryMethod(
+ """
+ @Query("UPDATE user set uid = :id + 1 where uid = :id")
+ abstract int incrementId(String id);
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_select() {
+ singleQueryMethod(
+ """
+ @Query("select * from user")
+ abstract int loadUsers();
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun query_detectTransaction_selectInTransaction() {
+ singleQueryMethod(
+ """
+ @Transaction
+ @Query("select * from user")
+ abstract int loadUsers();
+ """
+ ) { method, _ ->
+ assertThat(method.inTransaction, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun skipVerification() {
+ singleQueryMethod(
+ """
+ @SkipQueryVerification
+ @Query("SELECT foo from User")
+ abstract public int[] foo();
+ """) { parsedQuery, _ ->
+ assertThat(parsedQuery.name, `is`("foo"))
+ assertThat(parsedQuery.parameters.size, `is`(0))
+ assertThat(parsedQuery.returnType.typeName(),
+ `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun suppressWarnings() {
+ singleQueryMethod("""
+ @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+ @Query("SELECT uid from User")
+ abstract public int[] foo();
+ """) { method, invocation ->
+ assertThat(QueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = Mockito.mock(DeclaredType::class.java),
+ executableElement = method.element,
+ dbVerifier = null).context.logger.suppressedWarnings
+ , `is`(setOf(Warning.CURSOR_MISMATCH)))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun pojo_renamedColumn() {
+ pojoTest("""
+ String name;
+ String lName;
+ """, listOf("name", "lastName as lName")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_exactMatch() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("name", "lastName")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_exactMatchWithStar() {
+ pojoTest("""
+ String name;
+ String lastName;
+ int uid;
+ @ColumnInfo(name = "ageColumn")
+ int age;
+ """, listOf("*")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_nonJavaName() {
+ pojoTest("""
+ @ColumnInfo(name = "MAX(ageColumn)")
+ int maxAge;
+ String name;
+ """, listOf("MAX(ageColumn)", "name")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_noMatchingFields() {
+ pojoTest("""
+ String nameX;
+ String lastNameX;
+ """, listOf("name", "lastName")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("name", "lastName")))
+ assertThat(adapter?.mapping?.unusedFields, `is`(adapter?.pojo?.fields))
+ }?.failsToCompile()
+ ?.withErrorContaining(CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ ?.and()
+ ?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = listOf("name", "lastName"),
+ unusedFields = listOf(createField("nameX"),
+ createField("lastNameX")),
+ allColumns = listOf("name", "lastName"),
+ allFields = listOf(
+ createField("nameX"),
+ createField("lastNameX")
+ )
+ )
+ )
+ }
+
+ @Test
+ fun pojo_badQuery() {
+ // do not report mismatch if query is broken
+ pojoTest("""
+ @ColumnInfo(name = "MAX(ageColumn)")
+ int maxAge;
+ String name;
+ """, listOf("MAX(age)", "name")) { _, _, _ ->
+ }?.failsToCompile()
+ ?.withErrorContaining("no such column: age")
+ ?.and()
+ ?.withErrorCount(1)
+ ?.withWarningCount(0)
+ }
+
+ @Test
+ fun pojo_tooManyColumns() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("uid", "name", "lastName")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
+ assertThat(adapter?.mapping?.unusedFields, `is`(emptyList()))
+ }?.compilesWithoutError()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = listOf("uid"),
+ unusedFields = emptyList(),
+ allColumns = listOf("uid", "name", "lastName"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))
+ }
+
+ @Test
+ fun pojo_tooManyFields() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("lastName")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(
+ adapter?.pojo?.fields?.filter { it.name == "name" }
+ ))
+ }?.compilesWithoutError()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = emptyList(),
+ unusedFields = listOf(createField("name")),
+ allColumns = listOf("lastName"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))
+ }
+
+ @Test
+ fun pojo_missingNonNull() {
+ pojoTest("""
+ @NonNull
+ String name;
+ String lastName;
+ """, listOf("lastName")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList()))
+ assertThat(adapter?.mapping?.unusedFields, `is`(
+ adapter?.pojo?.fields?.filter { it.name == "name" }
+ ))
+ }?.failsToCompile()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = emptyList(),
+ unusedFields = listOf(createField("name")),
+ allColumns = listOf("lastName"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))?.and()?.withErrorContaining(
+ ProcessorErrors.pojoMissingNonNull(pojoTypeName = POJO,
+ missingPojoFields = listOf("name"),
+ allQueryColumns = listOf("lastName")))
+ }
+
+ @Test
+ fun pojo_tooManyFieldsAndColumns() {
+ pojoTest("""
+ String name;
+ String lastName;
+ """, listOf("uid", "name")) { adapter, _, _ ->
+ assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid")))
+ assertThat(adapter?.mapping?.unusedFields, `is`(
+ adapter?.pojo?.fields?.filter { it.name == "lastName" }
+ ))
+ }?.compilesWithoutError()?.withWarningContaining(
+ ProcessorErrors.cursorPojoMismatch(
+ pojoTypeName = POJO,
+ unusedColumns = listOf("uid"),
+ unusedFields = listOf(createField("lastName")),
+ allColumns = listOf("uid", "name"),
+ allFields = listOf(createField("name"), createField("lastName"))
+ ))
+ }
+
+ fun pojoTest(pojoFields: String, queryColumns: List<String>,
+ handler: (PojoRowAdapter?, QueryMethod, TestInvocation) -> Unit): CompileTester? {
+ val assertion = singleQueryMethod(
+ """
+ static class Pojo {
+ $pojoFields
+ }
+ @Query("SELECT ${queryColumns.joinToString(", ")} from User LIMIT 1")
+ abstract MyClass.Pojo getNameAndLastNames();
+ """
+ ) { parsedQuery, invocation ->
+ val adapter = parsedQuery.queryResultBinder.adapter
+ if (enableVerification) {
+ if (adapter is SingleEntityQueryResultAdapter) {
+ handler(adapter.rowAdapter as? PojoRowAdapter, parsedQuery, invocation)
+ } else {
+ handler(null, parsedQuery, invocation)
+ }
+ } else {
+ assertThat(adapter, nullValue())
+ }
+ }
+ if (enableVerification) {
+ return assertion
+ } else {
+ assertion.failsToCompile().withErrorContaining(CANNOT_FIND_QUERY_RESULT_ADAPTER)
+ return null
+ }
+ }
+
+ fun singleQueryMethod(vararg input: String,
+ handler: (QueryMethod, TestInvocation) -> Unit):
+ CompileTester {
+ return assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+ ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER, COMMON.BOOK))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Query::class, Dao::class, ColumnInfo::class,
+ Entity::class, PrimaryKey::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ it.hasAnnotation(Query::class)
+ }
+ )
+ }.filter { it.second.isNotEmpty() }.first()
+ val verifier = if (enableVerification) {
+ createVerifierFromEntities(invocation)
+ } else {
+ null
+ }
+ val parser = QueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()),
+ dbVerifier = verifier)
+ val parsedQuery = parser.process()
+ handler(parsedQuery, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
new file mode 100644
index 0000000..d6d6269
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2018 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 androidx.room.processor
+
+import COMMON
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.RawQuery
+import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.SupportDbTypeNames
+import androidx.room.ext.hasAnnotation
+import androidx.room.ext.typeName
+import androidx.room.processor.ProcessorErrors.RAW_QUERY_STRING_PARAMETER_REMOVED
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.RawQueryMethod
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+
+class RawQueryMethodProcessorTest {
+ @Test
+ fun supportRawQuery() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.returnType.typeName(),
+ `is`(ArrayTypeName.of(TypeName.INT) as TypeName))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun stringRawQuery() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(String query);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(RAW_QUERY_STRING_PARAMETER_REMOVED)
+ }
+
+ @Test
+ fun withObservedEntities() {
+ singleQueryMethod(
+ """
+ @RawQuery(observedEntities = User.class)
+ abstract public LiveData<User> foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.observedTableNames.size, `is`(1))
+ assertThat(query.observedTableNames, `is`(setOf("User")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun observableWithoutEntities() {
+ singleQueryMethod(
+ """
+ @RawQuery(observedEntities = {})
+ abstract public LiveData<User> foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.observedTableNames, `is`(emptySet()))
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+
+ @Test
+ fun observableWithoutEntities_dataSourceFactory() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, User> getOne();
+ """) { _, _ ->
+ // do nothing
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+
+ @Test
+ fun observableWithoutEntities_positionalDataSource() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
+ """) { _, _ ->
+ // do nothing
+ }.failsToCompile()
+ .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ }
+
+ @Test
+ fun positionalDataSource() {
+ singleQueryMethod(
+ """
+ @RawQuery(observedEntities = {User.class})
+ abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne(
+ SupportSQLiteQuery query);
+ """) { _, _ ->
+ // do nothing
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun pojo() {
+ val pojo: TypeName = ClassName.get("foo.bar.MyClass", "MyPojo")
+ singleQueryMethod(
+ """
+ public class MyPojo {
+ public String foo;
+ public String bar;
+ }
+
+ @RawQuery
+ abstract public MyPojo foo(SupportSQLiteQuery query);
+ """) { query, _ ->
+ assertThat(query.name, `is`("foo"))
+ assertThat(query.runtimeQueryParam, `is`(
+ RawQueryMethod.RuntimeQueryParameter(
+ paramName = "query",
+ type = SupportDbTypeNames.QUERY
+ )
+ ))
+ assertThat(query.returnType.typeName(), `is`(pojo))
+ assertThat(query.observedTableNames, `is`(emptySet()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun void() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public void foo(SupportSQLiteQuery query);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
+ )
+ }
+
+ @Test
+ fun noArgs() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo();
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+
+ @Test
+ fun tooManyArgs() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(SupportSQLiteQuery query,
+ SupportSQLiteQuery query2);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+
+ @Test
+ fun varargs() {
+ singleQueryMethod(
+ """
+ @RawQuery
+ abstract public int[] foo(SupportSQLiteQuery... query);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+
+ @Test
+ fun observed_notAnEntity() {
+ singleQueryMethod(
+ """
+ @RawQuery(observedEntities = {${COMMON.NOT_AN_ENTITY_TYPE_NAME}.class})
+ abstract public int[] foo(SupportSQLiteQuery query);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.rawQueryBadEntity(COMMON.NOT_AN_ENTITY_TYPE_NAME)
+ )
+ }
+
+ @Test
+ fun observed_relationPojo() {
+ singleQueryMethod(
+ """
+ public static class MyPojo {
+ public String foo;
+ @Relation(
+ parentColumn = "foo",
+ entityColumn = "name"
+ )
+ public java.util.List<User> users;
+ }
+ @RawQuery(observedEntities = MyPojo.class)
+ abstract public int[] foo(SupportSQLiteQuery query);
+ """) { method, _ ->
+ assertThat(method.observedTableNames, `is`(setOf("User")))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun observed_embedded() {
+ singleQueryMethod(
+ """
+ public static class MyPojo {
+ public String foo;
+ @Embedded
+ public User users;
+ }
+ @RawQuery(observedEntities = MyPojo.class)
+ abstract public int[] foo(SupportSQLiteQuery query);
+ """) { method, _ ->
+ assertThat(method.observedTableNames, `is`(setOf("User")))
+ }.compilesWithoutError()
+ }
+
+ private fun singleQueryMethod(
+ vararg input: String,
+ handler: (RawQueryMethod, TestInvocation) -> Unit
+ ): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ DAO_PREFIX
+ + input.joinToString("\n")
+ + DAO_SUFFIX
+ ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
+ COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
+ COMMON.NOT_AN_ENTITY))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Query::class, Dao::class, ColumnInfo::class,
+ Entity::class, PrimaryKey::class, RawQuery::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ it.hasAnnotation(RawQuery::class)
+ }
+ )
+ }.first { it.second.isNotEmpty() }
+ val parser = RawQueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ val parsedQuery = parser.process()
+ handler(parsedQuery, invocation)
+ true
+ }
+ .build())
+ }
+
+ companion object {
+ private const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+ import androidx.sqlite.db.SupportSQLiteQuery;
+ import androidx.lifecycle.LiveData;
+ @Dao
+ abstract class MyClass {
+ """
+ private const val DAO_SUFFIX = "}"
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
new file mode 100644
index 0000000..f771b28
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
@@ -0,0 +1,298 @@
+/*
+ * 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 androidx.room.processor
+
+import COMMON
+import androidx.room.Dao
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.typeName
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.ShortcutMethod
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+import kotlin.reflect.KClass
+
+/**
+ * Base test class for shortcut methods.
+ */
+abstract class ShortcutMethodProcessorTest<out T : ShortcutMethod>(
+ val annotation: KClass<out Annotation>) {
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ import java.util.*;
+ @Dao
+ abstract class MyClass {
+ """
+ const val DAO_SUFFIX = "}"
+ val USER_TYPE_NAME: TypeName = COMMON.USER_TYPE_NAME
+ val BOOK_TYPE_NAME: TypeName = ClassName.get("foo.bar", "Book")
+ }
+
+ @Test
+ fun noParams() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void foo();
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("foo"))
+ assertThat(shortcut.parameters.size, `is`(0))
+ assertThat(shortcut.returnCount, `is`(false))
+ }.failsToCompile().withErrorContaining(noParamsError())
+ }
+
+ abstract fun noParamsError(): String
+
+ @Test
+ fun single() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public int foo(User user);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("foo"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.type.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(shortcut.entities.size, `is`(1))
+ assertThat(shortcut.entities["user"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.returnCount, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun notAnEntity() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void foo(NotAnEntity notValid);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("foo"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.entityType, `is`(CoreMatchers.nullValue()))
+ assertThat(shortcut.entities.size, `is`(0))
+ }.failsToCompile().withErrorContaining(
+ ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
+ )
+ }
+
+ @Test
+ fun two() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void foo(User u1, User u2);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("foo"))
+
+ assertThat(shortcut.parameters.size, `is`(2))
+ shortcut.parameters.forEach {
+ assertThat(it.type.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(it.entityType?.typeName(), `is`(USER_TYPE_NAME))
+ }
+ assertThat(shortcut.entities.size, `is`(2))
+ assertThat(shortcut.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.parameters.map { it.name },
+ `is`(listOf("u1", "u2")))
+ assertThat(shortcut.returnCount, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun list() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public int users(List<User> users);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("users"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(
+ ClassName.get("java.util", "List"), USER_TYPE_NAME) as TypeName))
+ assertThat(param.entityType?.typeName(), `is`(USER_TYPE_NAME))
+ assertThat(shortcut.entities.size, `is`(1))
+ assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.returnCount, `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun array() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void users(User[] users);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("users"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ArrayTypeName.of(COMMON.USER_TYPE_NAME) as TypeName))
+ assertThat(shortcut.entities.size, `is`(1))
+ assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.returnCount, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun set() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void modifyUsers(Set<User> users);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("modifyUsers"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(ClassName.get("java.util", "Set")
+ , COMMON.USER_TYPE_NAME) as TypeName))
+ assertThat(shortcut.entities.size, `is`(1))
+ assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.returnCount, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun iterable() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void modifyUsers(Iterable<User> users);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("modifyUsers"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(ClassName.get("java.lang", "Iterable")
+ , COMMON.USER_TYPE_NAME) as TypeName))
+ assertThat(shortcut.entities.size, `is`(1))
+ assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.returnCount, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun customCollection() {
+ singleShortcutMethod(
+ """
+ static class MyList<Irrelevant, Item> extends ArrayList<Item> {}
+ @${annotation.java.canonicalName}
+ abstract public void modifyUsers(MyList<String, User> users);
+ """) { shortcut, _ ->
+ assertThat(shortcut.name, `is`("modifyUsers"))
+ assertThat(shortcut.parameters.size, `is`(1))
+ val param = shortcut.parameters.first()
+ assertThat(param.type.typeName(), `is`(
+ ParameterizedTypeName.get(ClassName.get("foo.bar", "MyClass.MyList")
+ , CommonTypeNames.STRING
+ , COMMON.USER_TYPE_NAME) as TypeName))
+ assertThat(shortcut.entities.size, `is`(1))
+ assertThat(shortcut.entities["users"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.returnCount, `is`(false))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun differentTypes() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public void foo(User u1, Book b1);
+ """) { shortcut, _ ->
+ assertThat(shortcut.parameters.size, `is`(2))
+ assertThat(shortcut.parameters[0].type.typeName().toString(),
+ `is`("foo.bar.User"))
+ assertThat(shortcut.parameters[1].type.typeName().toString(),
+ `is`("foo.bar.Book"))
+ assertThat(shortcut.parameters.map { it.name }, `is`(listOf("u1", "b1")))
+ assertThat(shortcut.returnCount, `is`(false))
+ assertThat(shortcut.entities.size, `is`(2))
+ assertThat(shortcut.entities["u1"]?.typeName, `is`(USER_TYPE_NAME))
+ assertThat(shortcut.entities["b1"]?.typeName, `is`(BOOK_TYPE_NAME))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun invalidReturnType() {
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}
+ abstract public long foo(User user);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(invalidReturnTypeError())
+ }
+
+ abstract fun invalidReturnTypeError(): String
+
+ abstract fun process(baseContext: Context, containing: DeclaredType,
+ executableElement: ExecutableElement): T
+
+ fun singleShortcutMethod(vararg input: String,
+ handler: (T, TestInvocation) -> Unit):
+ CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+ ), COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(annotation, Dao::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ MoreElements.isAnnotationPresent(it,
+ annotation.java)
+ }
+ )
+ }.filter { it.second.isNotEmpty() }.first()
+ val processed = process(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ handler(processed, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
new file mode 100644
index 0000000..d0c4fe3
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.Dao
+import androidx.room.Transaction
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.TransactionMethod
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class TransactionMethodProcessorTest {
+
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ import java.util.*;
+ @Dao
+ abstract class MyClass {
+ """
+ const val DAO_SUFFIX = "}"
+ }
+
+ @Test
+ fun simple() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ public String doInTransaction(int param) { return null; }
+ """) { transaction, _ ->
+ assertThat(transaction.name, `is`("doInTransaction"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun modifier_private() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ private String doInTransaction(int param) { return null; }
+ """) { transaction, _ ->
+ assertThat(transaction.name, `is`("doInTransaction"))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+ }
+
+ @Test
+ fun modifier_final() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ public final String doInTransaction(int param) { return null; }
+ """) { transaction, _ ->
+ assertThat(transaction.name, `is`("doInTransaction"))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+ }
+
+ private fun singleTransactionMethod(vararg input: String,
+ handler: (TransactionMethod, TestInvocation) -> Unit):
+ CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ TransactionMethodProcessorTest.DAO_PREFIX + input.joinToString("\n") +
+ TransactionMethodProcessorTest.DAO_SUFFIX
+ )))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Transaction::class, Dao::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ MoreElements.isAnnotationPresent(it,
+ Transaction::class.java)
+ }
+ )
+ }.filter { it.second.isNotEmpty() }.first()
+ val processor = TransactionMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ val processed = processor.process()
+ handler(processed, invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
new file mode 100644
index 0000000..75366e2
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.room.processor
+
+import androidx.room.OnConflictStrategy
+import androidx.room.Update
+import androidx.room.processor.ProcessorErrors
+ .UPDATE_METHODS_MUST_RETURN_VOID_OR_INT
+import androidx.room.processor.ProcessorErrors.UPDATE_MISSING_PARAMS
+import androidx.room.vo.UpdateMethod
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.type.DeclaredType
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class UpdateMethodProcessorTest : ShortcutMethodProcessorTest<UpdateMethod>(Update::class) {
+ override fun invalidReturnTypeError(): String = UPDATE_METHODS_MUST_RETURN_VOID_OR_INT
+
+ override fun noParamsError(): String = UPDATE_MISSING_PARAMS
+
+ override fun process(baseContext: Context, containing: DeclaredType,
+ executableElement: ExecutableElement): UpdateMethod {
+ return UpdateMethodProcessor(baseContext, containing, executableElement).process()
+ }
+
+ @Test
+ fun goodConflict() {
+ singleShortcutMethod(
+ """
+ @Update(onConflict = OnConflictStrategy.REPLACE)
+ abstract public void foo(User user);
+ """) { shortcut, _ ->
+ assertThat(shortcut.onConflictStrategy, `is`(OnConflictStrategy.REPLACE))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun badConflict() {
+ singleShortcutMethod(
+ """
+ @Update(onConflict = -1)
+ abstract public void foo(User user);
+ """) { _, _ ->
+ }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
new file mode 100644
index 0000000..4d2f940
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/BasicColumnTypeAdaptersTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver
+
+import androidx.room.processor.Context
+import androidx.room.testing.TestInvocation
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import simpleRun
+import testCodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.PrimitiveType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@RunWith(Parameterized::class)
+class BasicColumnTypeAdaptersTest(val input: Input, val bindCode: String,
+ val cursorCode: String) {
+ val scope = testCodeGenScope()
+
+ companion object {
+ val SQLITE_STMT: TypeName = ClassName.get("android.database.sqlite", "SQLiteStatement")
+ val CURSOR: TypeName = ClassName.get("android.database", "Cursor")
+
+ @Parameterized.Parameters(name = "kind:{0},bind:_{1},cursor:_{2}")
+ @JvmStatic
+ fun params(): List<Array<Any>> {
+ return listOf(
+ arrayOf(Input(TypeKind.INT),
+ "st.bindLong(6, inp);",
+ "out = crs.getInt(9);"),
+ arrayOf(Input(TypeKind.BYTE),
+ "st.bindLong(6, inp);",
+ "out = (byte) crs.getShort(9);"),
+ arrayOf(Input(TypeKind.SHORT),
+ "st.bindLong(6, inp);",
+ "out = crs.getShort(9);"),
+ arrayOf(Input(TypeKind.LONG),
+ "st.bindLong(6, inp);",
+ "out = crs.getLong(9);"),
+ arrayOf(Input(TypeKind.CHAR),
+ "st.bindLong(6, inp);",
+ "out = (char) crs.getInt(9);"),
+ arrayOf(Input(TypeKind.FLOAT),
+ "st.bindDouble(6, inp);",
+ "out = crs.getFloat(9);"),
+ arrayOf(Input(TypeKind.DOUBLE),
+ "st.bindDouble(6, inp);",
+ "out = crs.getDouble(9);"),
+ arrayOf(Input(TypeKind.DECLARED, "java.lang.String"),
+ """
+ if (inp == null) {
+ st.bindNull(6);
+ } else {
+ st.bindString(6, inp);
+ }
+ """.trimIndent(),
+ "out = crs.getString(9);")
+ )
+ }
+ }
+
+ @Test
+ fun bind() {
+ simpleRun { invocation ->
+ val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
+ .findColumnTypeAdapter(input.getTypeMirror(invocation.processingEnv), null)!!
+ adapter.bindToStmt("st", "6", "inp", scope)
+ assertThat(scope.generate().trim(), `is`(bindCode))
+ generateCode(invocation, false)
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun boxedBind() {
+ if (!input.typeKind.isPrimitive) {
+ return // no-op for those
+ }
+ simpleRun { invocation ->
+ val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
+ .findColumnTypeAdapter(
+ input.getBoxedTypeMirror(invocation.processingEnv), null)!!
+ adapter.bindToStmt("st", "6", "inp", scope)
+ assertThat(scope.generate().trim(), `is`(
+ """
+ if (inp == null) {
+ st.bindNull(6);
+ } else {
+ $bindCode
+ }
+ """.trimIndent()
+ ))
+ generateCode(invocation, true)
+ }.compilesWithoutError()
+ }
+
+ private fun generateCode(invocation: TestInvocation, boxed: Boolean) {
+ val typeMirror = if (boxed) input.getBoxedTypeMirror(invocation.processingEnv)
+ else input.getTypeMirror(invocation.processingEnv)
+ val spec = TypeSpec.classBuilder("OutClass")
+ .addField(FieldSpec.builder(SQLITE_STMT, "st").build())
+ .addField(FieldSpec.builder(CURSOR, "crs").build())
+ .addField(FieldSpec.builder(TypeName.get(typeMirror), "out").build())
+ .addField(FieldSpec.builder(TypeName.get(typeMirror), "inp").build())
+ .addMethod(
+ MethodSpec.methodBuilder("foo")
+ .addCode(scope.builder().build())
+ .build()
+ )
+ .build()
+ JavaFile.builder("foo.bar", spec).build().writeTo(invocation.processingEnv.filer)
+ }
+
+ @Test
+ fun read() {
+ simpleRun { invocation ->
+ val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
+ .findColumnTypeAdapter(input.getTypeMirror(invocation.processingEnv), null)!!
+ adapter.readFromCursor("out", "crs", "9", scope)
+ assertThat(scope.generate().trim(), `is`(cursorCode))
+ generateCode(invocation, false)
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun readBoxed() {
+ if (!input.typeKind.isPrimitive) {
+ return // no-op for those
+ }
+ simpleRun { invocation ->
+ val adapter = TypeAdapterStore.create(Context(invocation.processingEnv))
+ .findColumnTypeAdapter(
+ input.getBoxedTypeMirror(invocation.processingEnv), null)!!
+ adapter.readFromCursor("out", "crs", "9", scope)
+ assertThat(scope.generate().trim(), `is`(
+ """
+ if (crs.isNull(9)) {
+ out = null;
+ } else {
+ $cursorCode
+ }
+ """.trimIndent()
+ ))
+ generateCode(invocation, true)
+ }.compilesWithoutError()
+ }
+
+ data class Input(val typeKind: TypeKind, val qName: String? = null) {
+ fun getTypeMirror(processingEnv: ProcessingEnvironment): TypeMirror {
+ return if (typeKind.isPrimitive) {
+ processingEnv.typeUtils.getPrimitiveType(typeKind)
+ } else {
+ processingEnv.elementUtils.getTypeElement(qName).asType()
+ }
+ }
+
+ fun getBoxedTypeMirror(processingEnv: ProcessingEnvironment): TypeMirror {
+ return if (typeKind.isPrimitive) {
+ processingEnv.typeUtils
+ .boxedClass(getTypeMirror(processingEnv) as PrimitiveType)
+ .asType()
+ } else {
+ getTypeMirror(processingEnv)
+ }
+ }
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt
new file mode 100644
index 0000000..196fc41
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/CustomTypeConverterResolutionTest.kt
@@ -0,0 +1,354 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("HasPlatformType")
+
+package androidx.room.solver
+
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.RoomProcessor
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.S
+import androidx.room.ext.T
+import androidx.room.processor.ProcessorErrors.CANNOT_BIND_QUERY_PARAMETER_INTO_STMT
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.lang.model.element.Modifier
+import javax.tools.JavaFileObject
+
+@RunWith(JUnit4::class)
+class CustomTypeConverterResolutionTest {
+ fun TypeSpec.toJFO(): JavaFileObject {
+ return JavaFileObjects.forSourceString("foo.bar.${this.name}",
+ "package foo.bar;\n" + toString())
+ }
+
+ companion object {
+ val ENTITY = ClassName.get("foo.bar", "MyEntity")
+ val DB = ClassName.get("foo.bar", "MyDb")
+ val DAO = ClassName.get("foo.bar", "MyDao")
+
+ val CUSTOM_TYPE = ClassName.get("foo.bar", "CustomType")
+ val CUSTOM_TYPE_JFO = JavaFileObjects.forSourceLines(CUSTOM_TYPE.toString(),
+ """
+ package ${CUSTOM_TYPE.packageName()};
+ public class ${CUSTOM_TYPE.simpleName()} {
+ public int value;
+ }
+ """)
+ val CUSTOM_TYPE_CONVERTER = ClassName.get("foo.bar", "MyConverter")
+ val CUSTOM_TYPE_CONVERTER_JFO = JavaFileObjects.forSourceLines(
+ CUSTOM_TYPE_CONVERTER.toString(),
+ """
+ package ${CUSTOM_TYPE_CONVERTER.packageName()};
+ public class ${CUSTOM_TYPE_CONVERTER.simpleName()} {
+ @${TypeConverter::class.java.canonicalName}
+ public static $CUSTOM_TYPE toCustom(int value) {
+ return null;
+ }
+ @${TypeConverter::class.java.canonicalName}
+ public static int fromCustom($CUSTOM_TYPE input) {
+ return 0;
+ }
+ }
+ """)
+ val CUSTOM_TYPE_SET = ParameterizedTypeName.get(
+ ClassName.get(Set::class.java), CUSTOM_TYPE)
+ val CUSTOM_TYPE_SET_CONVERTER = ClassName.get("foo.bar", "MySetConverter")
+ val CUSTOM_TYPE_SET_CONVERTER_JFO = JavaFileObjects.forSourceLines(
+ CUSTOM_TYPE_SET_CONVERTER.toString(),
+ """
+ package ${CUSTOM_TYPE_SET_CONVERTER.packageName()};
+ import java.util.HashSet;
+ import java.util.Set;
+ public class ${CUSTOM_TYPE_SET_CONVERTER.simpleName()} {
+ @${TypeConverter::class.java.canonicalName}
+ public static $CUSTOM_TYPE_SET toCustom(int value) {
+ return null;
+ }
+ @${TypeConverter::class.java.canonicalName}
+ public static int fromCustom($CUSTOM_TYPE_SET input) {
+ return 0;
+ }
+ }
+ """)
+ }
+
+ @Test
+ fun useFromDatabase_forEntity() {
+ val entity = createEntity(hasCustomField = true)
+ val database = createDatabase(hasConverters = true, hasDao = true)
+ val dao = createDao(hasQueryReturningEntity = true, hasQueryWithCustomParam = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun collection_forEntity() {
+ val entity = createEntity(
+ hasCustomField = true,
+ useCollection = true)
+ val database = createDatabase(
+ hasConverters = true,
+ hasDao = true,
+ useCollection = true)
+ val dao = createDao(
+ hasQueryWithCustomParam = false,
+ useCollection = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun collection_forDao() {
+ val entity = createEntity(
+ hasCustomField = true,
+ useCollection = true)
+ val database = createDatabase(
+ hasConverters = true,
+ hasDao = true,
+ useCollection = true)
+ val dao = createDao(
+ hasQueryWithCustomParam = true,
+ useCollection = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromDatabase_forQueryParameter() {
+ val entity = createEntity()
+ val database = createDatabase(hasConverters = true, hasDao = true)
+ val dao = createDao(hasQueryWithCustomParam = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromDatabase_forReturnValue() {
+ val entity = createEntity(hasCustomField = true)
+ val database = createDatabase(hasConverters = true, hasDao = true)
+ val dao = createDao(hasQueryReturningEntity = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromDao_forQueryParameter() {
+ val entity = createEntity()
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasConverters = true, hasQueryReturningEntity = true,
+ hasQueryWithCustomParam = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromEntity_forReturnValue() {
+ val entity = createEntity(hasCustomField = true, hasConverters = true)
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasQueryReturningEntity = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromEntityField_forReturnValue() {
+ val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasQueryReturningEntity = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromEntity_forQueryParameter() {
+ val entity = createEntity(hasCustomField = true, hasConverters = true)
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasQueryWithCustomParam = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO())
+ .failsToCompile().withErrorContaining(CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
+ }
+
+ @Test
+ fun useFromEntityField_forQueryParameter() {
+ val entity = createEntity(hasCustomField = true, hasConverterOnField = true)
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasQueryWithCustomParam = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO())
+ .failsToCompile().withErrorContaining(CANNOT_BIND_QUERY_PARAMETER_INTO_STMT)
+ }
+
+ @Test
+ fun useFromQueryMethod_forQueryParameter() {
+ val entity = createEntity()
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasQueryWithCustomParam = true, hasMethodConverters = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ @Test
+ fun useFromQueryParameter_forQueryParameter() {
+ val entity = createEntity()
+ val database = createDatabase(hasDao = true)
+ val dao = createDao(hasQueryWithCustomParam = true, hasParameterConverters = true)
+ run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+ }
+
+ fun run(vararg jfos: JavaFileObject): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO
+ + CUSTOM_TYPE_SET_CONVERTER_JFO)
+ .processedWith(RoomProcessor())
+ }
+
+ private fun createEntity(
+ hasCustomField: Boolean = false,
+ hasConverters: Boolean = false,
+ hasConverterOnField: Boolean = false,
+ useCollection: Boolean = false): TypeSpec {
+ if (hasConverterOnField && hasConverters) {
+ throw IllegalArgumentException("cannot have both converters")
+ }
+ val type = if (useCollection) {
+ CUSTOM_TYPE_SET
+ } else {
+ CUSTOM_TYPE
+ }
+ return TypeSpec.classBuilder(ENTITY).apply {
+ addAnnotation(Entity::class.java)
+ addModifiers(Modifier.PUBLIC)
+ if (hasCustomField) {
+ addField(FieldSpec.builder(type, "myCustomField", Modifier.PUBLIC).apply {
+ if (hasConverterOnField) {
+ addAnnotation(createConvertersAnnotation())
+ }
+ }.build())
+ }
+ if (hasConverters) {
+ addAnnotation(createConvertersAnnotation())
+ }
+ addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
+ addAnnotation(PrimaryKey::class.java)
+ }.build())
+ }.build()
+ }
+
+ private fun createDatabase(
+ hasConverters: Boolean = false,
+ hasDao: Boolean = false,
+ useCollection: Boolean = false): TypeSpec {
+ return TypeSpec.classBuilder(DB).apply {
+ addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
+ superclass(RoomTypeNames.ROOM_DB)
+ if (hasConverters) {
+ addAnnotation(createConvertersAnnotation(useCollection = useCollection))
+ }
+ addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
+ addAnnotation(PrimaryKey::class.java)
+ }.build())
+ if (hasDao) {
+ addMethod(MethodSpec.methodBuilder("getDao").apply {
+ addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
+ returns(DAO)
+ }.build())
+ }
+ addAnnotation(
+ AnnotationSpec.builder(Database::class.java).apply {
+ addMember("entities", "{$T.class}", ENTITY)
+ addMember("version", "42")
+ }.build()
+ )
+ }.build()
+ }
+
+ private fun createDao(
+ hasConverters: Boolean = false,
+ hasQueryReturningEntity: Boolean = false,
+ hasQueryWithCustomParam: Boolean = false,
+ hasMethodConverters: Boolean = false,
+ hasParameterConverters: Boolean = false,
+ useCollection: Boolean = false): TypeSpec {
+ val annotationCount = listOf(hasMethodConverters, hasConverters, hasParameterConverters)
+ .map { if (it) 1 else 0 }.sum()
+ if (annotationCount > 1) {
+ throw IllegalArgumentException("cannot set both of these")
+ }
+ if (hasParameterConverters && !hasQueryWithCustomParam) {
+ throw IllegalArgumentException("inconsistent")
+ }
+ return TypeSpec.classBuilder(DAO).apply {
+ addAnnotation(Dao::class.java)
+ addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
+ if (hasConverters) {
+ addAnnotation(createConvertersAnnotation(useCollection = useCollection))
+ }
+ if (hasQueryReturningEntity) {
+ addMethod(MethodSpec.methodBuilder("loadAll").apply {
+ addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
+ addMember("value", S, "SELECT * FROM ${ENTITY.simpleName()} LIMIT 1")
+ }.build())
+ addModifiers(Modifier.ABSTRACT)
+ returns(ENTITY)
+ }.build())
+ }
+ val customType = if (useCollection) {
+ CUSTOM_TYPE_SET
+ } else {
+ CUSTOM_TYPE
+ }
+ if (hasQueryWithCustomParam) {
+ addMethod(MethodSpec.methodBuilder("queryWithCustom").apply {
+ addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
+ addMember("value", S, "SELECT COUNT(*) FROM ${ENTITY.simpleName()} where" +
+ " id = :custom")
+ }.build())
+ if (hasMethodConverters) {
+ addAnnotation(createConvertersAnnotation(useCollection = useCollection))
+ }
+ addParameter(ParameterSpec.builder(customType, "custom").apply {
+ if (hasParameterConverters) {
+ addAnnotation(createConvertersAnnotation(useCollection = useCollection))
+ }
+ }.build())
+ addModifiers(Modifier.ABSTRACT)
+ returns(TypeName.INT)
+ }.build())
+ }
+ }.build()
+ }
+
+ private fun createConvertersAnnotation(useCollection: Boolean = false): AnnotationSpec {
+ val converter = if (useCollection) {
+ CUSTOM_TYPE_SET_CONVERTER
+ } else {
+ CUSTOM_TYPE_CONVERTER
+ }
+ return AnnotationSpec.builder(TypeConverters::class.java)
+ .addMember("value", "$T.class", converter).build()
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
new file mode 100644
index 0000000..c512c21
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver
+
+import COMMON
+import androidx.paging.DataSource
+import androidx.paging.PositionalDataSource
+import androidx.room.Entity
+import androidx.room.ext.L
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.ReactiveStreamsTypeNames
+import androidx.room.ext.RoomTypeNames.STRING_UTIL
+import androidx.room.ext.RxJava2TypeNames
+import androidx.room.ext.T
+import androidx.room.ext.typeName
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
+import androidx.room.solver.binderprovider.DataSourceQueryResultBinderProvider
+import androidx.room.solver.binderprovider.FlowableQueryResultBinderProvider
+import androidx.room.solver.binderprovider.LiveDataQueryResultBinderProvider
+import androidx.room.solver.types.CompositeAdapter
+import androidx.room.solver.types.TypeConverter
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.TypeName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import simpleRun
+import testCodeGenScope
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class TypeAdapterStoreTest {
+ companion object {
+ fun tmp(index: Int) = CodeGenScope._tmpVar(index)
+ }
+
+ @Test
+ fun testDirect() {
+ singleRun { invocation ->
+ val store = TypeAdapterStore.create(Context(invocation.processingEnv))
+ val primitiveType = invocation.processingEnv.typeUtils.getPrimitiveType(TypeKind.INT)
+ val adapter = store.findColumnTypeAdapter(primitiveType, null)
+ assertThat(adapter, notNullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testJavaLangBoolean() {
+ singleRun { invocation ->
+ val store = TypeAdapterStore.create(Context(invocation.processingEnv))
+ val boolean = invocation
+ .processingEnv
+ .elementUtils
+ .getTypeElement("java.lang.Boolean")
+ .asType()
+ val adapter = store.findColumnTypeAdapter(boolean, null)
+ assertThat(adapter, notNullValue())
+ assertThat(adapter, instanceOf(CompositeAdapter::class.java))
+ val composite = adapter as CompositeAdapter
+ assertThat(composite.intoStatementConverter?.from?.typeName(),
+ `is`(TypeName.BOOLEAN.box()))
+ assertThat(composite.columnTypeAdapter.out.typeName(),
+ `is`(TypeName.INT.box()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testVia1TypeAdapter() {
+ singleRun { invocation ->
+ val store = TypeAdapterStore.create(Context(invocation.processingEnv))
+ val booleanType = invocation.processingEnv.typeUtils
+ .getPrimitiveType(TypeKind.BOOLEAN)
+ val adapter = store.findColumnTypeAdapter(booleanType, null)
+ assertThat(adapter, notNullValue())
+ assertThat(adapter, instanceOf(CompositeAdapter::class.java))
+ val bindScope = testCodeGenScope()
+ adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
+ assertThat(bindScope.generate().trim(), `is`("""
+ final int ${tmp(0)};
+ ${tmp(0)} = fooVar ? 1 : 0;
+ stmt.bindLong(41, ${tmp(0)});
+ """.trimIndent()))
+
+ val cursorScope = testCodeGenScope()
+ adapter.readFromCursor("res", "curs", "7", cursorScope)
+ assertThat(cursorScope.generate().trim(), `is`("""
+ final int ${tmp(0)};
+ ${tmp(0)} = curs.getInt(7);
+ res = ${tmp(0)} != 0;
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testVia2TypeAdapters() {
+ singleRun { invocation ->
+ val store = TypeAdapterStore.create(Context(invocation.processingEnv),
+ pointTypeConverters(invocation.processingEnv))
+ val pointType = invocation.processingEnv.elementUtils
+ .getTypeElement("foo.bar.Point").asType()
+ val adapter = store.findColumnTypeAdapter(pointType, null)
+ assertThat(adapter, notNullValue())
+ assertThat(adapter, instanceOf(CompositeAdapter::class.java))
+
+ val bindScope = testCodeGenScope()
+ adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
+ assertThat(bindScope.generate().trim(), `is`("""
+ final int ${tmp(0)};
+ final boolean ${tmp(1)};
+ ${tmp(1)} = foo.bar.Point.toBoolean(fooVar);
+ ${tmp(0)} = ${tmp(1)} ? 1 : 0;
+ stmt.bindLong(41, ${tmp(0)});
+ """.trimIndent()))
+
+ val cursorScope = testCodeGenScope()
+ adapter.readFromCursor("res", "curs", "11", cursorScope).toString()
+ assertThat(cursorScope.generate().trim(), `is`("""
+ final int ${tmp(0)};
+ ${tmp(0)} = curs.getInt(11);
+ final boolean ${tmp(1)};
+ ${tmp(1)} = ${tmp(0)} != 0;
+ res = foo.bar.Point.fromBoolean(${tmp(1)});
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testDate() {
+ singleRun { (processingEnv) ->
+ val store = TypeAdapterStore.create(Context(processingEnv),
+ dateTypeConverters(processingEnv))
+ val tDate = processingEnv.elementUtils.getTypeElement("java.util.Date").asType()
+ val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER)
+ assertThat(adapter, notNullValue())
+ assertThat(adapter?.typeMirror(), `is`(tDate))
+ val bindScope = testCodeGenScope()
+ adapter!!.readFromCursor("outDate", "curs", "0", bindScope)
+ assertThat(bindScope.generate().trim(), `is`("""
+ final java.lang.Long _tmp;
+ if (curs.isNull(0)) {
+ _tmp = null;
+ } else {
+ _tmp = curs.getLong(0);
+ }
+ // convert Long to Date;
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testIntList() {
+ singleRun { invocation ->
+ val binders = createIntListToStringBinders(invocation)
+ val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0],
+ binders[1])
+
+ val adapter = store.findColumnTypeAdapter(binders[0].from, null)
+ assertThat(adapter, notNullValue())
+
+ val bindScope = testCodeGenScope()
+ adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
+ assertThat(bindScope.generate().trim(), `is`("""
+ final java.lang.String ${tmp(0)};
+ ${tmp(0)} = androidx.room.util.StringUtil.joinIntoString(fooVar);
+ if (${tmp(0)} == null) {
+ stmt.bindNull(41);
+ } else {
+ stmt.bindString(41, ${tmp(0)});
+ }
+ """.trimIndent()))
+
+ val converter = store.findTypeConverter(binders[0].from,
+ invocation.context.COMMON_TYPES.STRING)
+ assertThat(converter, notNullValue())
+ assertThat(store.reverse(converter!!), `is`(binders[1]))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testOneWayConversion() {
+ singleRun { invocation ->
+ val binders = createIntListToStringBinders(invocation)
+ val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0])
+ val adapter = store.findColumnTypeAdapter(binders[0].from, null)
+ assertThat(adapter, nullValue())
+
+ val stmtBinder = store.findStatementValueBinder(binders[0].from, null)
+ assertThat(stmtBinder, notNullValue())
+
+ val converter = store.findTypeConverter(binders[0].from,
+ invocation.context.COMMON_TYPES.STRING)
+ assertThat(converter, notNullValue())
+ assertThat(store.reverse(converter!!), nullValue())
+ }
+ }
+
+ @Test
+ fun testMissingRxRoom() {
+ simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE)) { invocation ->
+ val publisherElement = invocation.processingEnv.elementUtils
+ .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString())
+ assertThat(publisherElement, notNullValue())
+ assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(publisherElement.asType())), `is`(true))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+ }
+
+ @Test
+ fun testFindPublisher() {
+ simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) {
+ invocation ->
+ val publisher = invocation.processingEnv.elementUtils
+ .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString())
+ assertThat(publisher, notNullValue())
+ assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(publisher.asType())), `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testFindFlowable() {
+ simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) {
+ invocation ->
+ val flowable = invocation.processingEnv.elementUtils
+ .getTypeElement(RxJava2TypeNames.FLOWABLE.toString())
+ assertThat(flowable, notNullValue())
+ assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(flowable.asType())), `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testFindLiveData() {
+ simpleRun(jfos = *arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) {
+ invocation ->
+ val liveData = invocation.processingEnv.elementUtils
+ .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString())
+ assertThat(liveData, notNullValue())
+ assertThat(LiveDataQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(liveData.asType())), `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun findDataSource() {
+ simpleRun {
+ invocation ->
+ val dataSource = invocation.processingEnv.elementUtils
+ .getTypeElement(DataSource::class.java.canonicalName)
+ assertThat(dataSource, notNullValue())
+ assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(dataSource.asType())), `is`(true))
+ }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
+ }
+
+ @Test
+ fun findPositionalDataSource() {
+ simpleRun {
+ invocation ->
+ val dataSource = invocation.processingEnv.elementUtils
+ .getTypeElement(PositionalDataSource::class.java.canonicalName)
+ assertThat(dataSource, notNullValue())
+ assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(dataSource.asType())), `is`(true))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun findDataSourceFactory() {
+ simpleRun(jfos = COMMON.DATA_SOURCE_FACTORY) {
+ invocation ->
+ val pagedListProvider = invocation.processingEnv.elementUtils
+ .getTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY.toString())
+ assertThat(pagedListProvider, notNullValue())
+ assertThat(DataSourceFactoryQueryResultBinderProvider(invocation.context).matches(
+ MoreTypes.asDeclared(pagedListProvider.asType())), `is`(true))
+ }.compilesWithoutError()
+ }
+
+ private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> {
+ val intType = invocation.processingEnv.elementUtils
+ .getTypeElement(Integer::class.java.canonicalName)
+ .asType()
+ val listType = invocation.processingEnv.elementUtils
+ .getTypeElement(java.util.List::class.java.canonicalName)
+ val listOfInts = invocation.processingEnv.typeUtils.getDeclaredType(listType, intType)
+
+ val intListConverter = object : TypeConverter(listOfInts,
+ invocation.context.COMMON_TYPES.STRING) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ addStatement("$L = $T.joinIntoString($L)", outputVarName, STRING_UTIL,
+ inputVarName)
+ }
+ }
+ }
+
+ val stringToIntListConverter = object : TypeConverter(
+ invocation.context.COMMON_TYPES.STRING, listOfInts) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ addStatement("$L = $T.splitToIntList($L)", outputVarName, STRING_UTIL,
+ inputVarName)
+ }
+ }
+ }
+ return listOf(intListConverter, stringToIntListConverter)
+ }
+
+ fun singleRun(handler: (TestInvocation) -> Unit): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.DummyClass",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class DummyClass {}
+ """
+ ), JavaFileObjects.forSourceString("foo.bar.Point",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class Point {
+ public int x, y;
+ public Point(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+ public static Point fromBoolean(boolean val) {
+ return val ? new Point(1, 1) : new Point(0, 0);
+ }
+ public static boolean toBoolean(Point point) {
+ return point.x > 0;
+ }
+ }
+ """
+ )))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Entity::class)
+ .nextRunHandler { invocation ->
+ handler(invocation)
+ true
+ }
+ .build())
+ }
+
+ fun pointTypeConverters(env: ProcessingEnvironment): List<TypeConverter> {
+ val tPoint = env.elementUtils.getTypeElement("foo.bar.Point").asType()
+ val tBoolean = env.typeUtils.getPrimitiveType(TypeKind.BOOLEAN)
+ return listOf(
+ object : TypeConverter(tPoint, tBoolean) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ addStatement("$L = $T.toBoolean($L)", outputVarName, from, inputVarName)
+ }
+ }
+ },
+ object : TypeConverter(tBoolean, tPoint) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ addStatement("$L = $T.fromBoolean($L)", outputVarName, tPoint,
+ inputVarName)
+ }
+ }
+ }
+ )
+ }
+
+ fun dateTypeConverters(env: ProcessingEnvironment): List<TypeConverter> {
+ val tDate = env.elementUtils.getTypeElement("java.util.Date").asType()
+ val tLong = env.elementUtils.getTypeElement("java.lang.Long").asType()
+ return listOf(
+ object : TypeConverter(tDate, tLong) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ addStatement("// convert Date to Long")
+ }
+ }
+ },
+ object : TypeConverter(tLong, tDate) {
+ override fun convert(inputVarName: String, outputVarName: String,
+ scope: CodeGenScope) {
+ scope.builder().apply {
+ addStatement("// convert Long to Date")
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAssignmentTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAssignmentTest.kt
new file mode 100644
index 0000000..4dfa839
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAssignmentTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 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 androidx.room.solver
+
+import androidx.room.ext.getAllFieldsIncludingPrivateSupers
+import androidx.room.ext.isAssignableWithoutVariance
+import androidx.room.testing.TestInvocation
+import com.google.testing.compile.JavaFileObjects
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import simpleRun
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+
+class TypeAssignmentTest {
+ companion object {
+ private val TEST_OBJECT = JavaFileObjects.forSourceString("foo.bar.MyObject",
+ """
+ package foo.bar;
+ import java.util.Set;
+ import java.util.HashSet;
+ import java.util.Map;
+ class MyObject {
+ String mString;
+ Integer mInteger;
+ Set<MyObject> mSet;
+ Set<? extends MyObject> mVarianceSet;
+ HashSet<MyObject> mHashSet;
+ Map<String, ?> mUnboundedMap;
+ Map<String, String> mStringMap;
+ }
+ """.trimIndent())
+ }
+
+ @Test
+ fun basic() {
+ runTest {
+ val testObject = typeElement("foo.bar.MyObject")
+ val string = testObject.getField(processingEnv, "mString")
+ val integer = testObject.getField(processingEnv, "mInteger")
+ assertThat(typeUtils.isAssignableWithoutVariance(string.asType(),
+ integer.asType()),
+ `is`(false))
+ }
+ }
+
+ @Test
+ fun generics() {
+ runTest {
+ val testObject = typeElement("foo.bar.MyObject")
+ val set = testObject.getField(processingEnv, "mSet").asType()
+ val hashSet = testObject.getField(processingEnv, "mHashSet").asType()
+ assertThat(typeUtils.isAssignableWithoutVariance(
+ from = set,
+ to = hashSet
+ ), `is`(false))
+ assertThat(typeUtils.isAssignableWithoutVariance(
+ from = hashSet,
+ to = set
+ ), `is`(true))
+ }
+ }
+
+ @Test
+ fun variance() {
+ /**
+ * Set<User> userSet = null;
+ * Set<? extends User> userSet2 = null;
+ * userSet = userSet2; // NOT OK for java but kotlin data classes hit this so we want
+ * // to accept it
+ */
+ runTest {
+ val testObject = typeElement("foo.bar.MyObject")
+ val set = testObject.getField(processingEnv, "mSet").asType()
+ val varianceSet = testObject.getField(processingEnv, "mVarianceSet").asType()
+ assertThat(typeUtils.isAssignableWithoutVariance(
+ from = set,
+ to = varianceSet
+ ), `is`(true))
+ assertThat(typeUtils.isAssignableWithoutVariance(
+ from = varianceSet,
+ to = set
+ ), `is`(true))
+ }
+ }
+
+ @Test
+ fun unboundedVariance() {
+ runTest {
+ val testObject = typeElement("foo.bar.MyObject")
+ val unbounded = testObject.getField(processingEnv, "mUnboundedMap").asType()
+ val objectMap = testObject.getField(processingEnv, "mStringMap").asType()
+ assertThat(typeUtils.isAssignableWithoutVariance(
+ from = unbounded,
+ to = objectMap
+ ), `is`(false))
+ assertThat(typeUtils.isAssignableWithoutVariance(
+ from = objectMap,
+ to = unbounded
+ ), `is`(true))
+ }
+ }
+
+ private fun TypeElement.getField(
+ env: ProcessingEnvironment,
+ name: String): VariableElement {
+ return getAllFieldsIncludingPrivateSupers(env).first {
+ it.simpleName.toString() == name
+ }
+ }
+
+ private fun runTest(handler: TestInvocation.() -> Unit) {
+ simpleRun(TEST_OBJECT) {
+ it.apply { handler() }
+ }.compilesWithoutError()
+ }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
new file mode 100644
index 0000000..4f37cfa
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.solver.query
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.ext.RoomTypeNames.ROOM_SQL_QUERY
+import androidx.room.ext.RoomTypeNames.STRING_UTIL
+import androidx.room.processor.QueryMethodProcessor
+import androidx.room.testing.TestProcessor
+import androidx.room.writer.QueryWriter
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourceSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import testCodeGenScope
+
+@RunWith(JUnit4::class)
+class QueryWriterTest {
+ companion object {
+ const val DAO_PREFIX = """
+ package foo.bar;
+ import androidx.room.*;
+ import java.util.*;
+ @Dao
+ abstract class MyClass {
+ """
+ const val DAO_SUFFIX = "}"
+ val QUERY = ROOM_SQL_QUERY.toString()
+ }
+
+ @Test
+ fun simpleNoArgQuery() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users")
+ abstract java.util.List<Integer> selectAllIds();
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`("""
+ final java.lang.String _sql = "SELECT id FROM users";
+ final $QUERY _stmt = $QUERY.acquire(_sql, 0);
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun simpleStringArgs() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE name LIKE :name")
+ abstract java.util.List<Integer> selectAllIds(String name);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`(
+ """
+ final java.lang.String _sql = "SELECT id FROM users WHERE name LIKE ?";
+ final $QUERY _stmt = $QUERY.acquire(_sql, 1);
+ int _argIndex = 1;
+ if (name == null) {
+ _stmt.bindNull(_argIndex);
+ } else {
+ _stmt.bindString(_argIndex, name);
+ }
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun twoIntArgs() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE id IN(:id1,:id2)")
+ abstract java.util.List<Integer> selectAllIds(int id1, int id2);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`(
+ """
+ final java.lang.String _sql = "SELECT id FROM users WHERE id IN(?,?)";
+ final $QUERY _stmt = $QUERY.acquire(_sql, 2);
+ int _argIndex = 1;
+ _stmt.bindLong(_argIndex, id1);
+ _argIndex = 2;
+ _stmt.bindLong(_argIndex, id2);
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun aLongAndIntVarArg() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE id IN(:ids) AND age > :time")
+ abstract java.util.List<Integer> selectAllIds(long time, int... ids);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`(
+ """
+ java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
+ _stringBuilder.append("SELECT id FROM users WHERE id IN(");
+ final int _inputSize = ids.length;
+ $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(") AND age > ");
+ _stringBuilder.append("?");
+ final java.lang.String _sql = _stringBuilder.toString();
+ final int _argCount = 1 + _inputSize;
+ final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (int _item : ids) {
+ _stmt.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ _argIndex = 1 + _inputSize;
+ _stmt.bindLong(_argIndex, time);
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ val collectionOut = """
+ java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
+ _stringBuilder.append("SELECT id FROM users WHERE id IN(");
+ final int _inputSize = ids.size();
+ $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(") AND age > ");
+ _stringBuilder.append("?");
+ final java.lang.String _sql = _stringBuilder.toString();
+ final int _argCount = 1 + _inputSize;
+ final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (java.lang.Integer _item : ids) {
+ if (_item == null) {
+ _stmt.bindNull(_argIndex);
+ } else {
+ _stmt.bindLong(_argIndex, _item);
+ }
+ _argIndex ++;
+ }
+ _argIndex = 1 + _inputSize;
+ _stmt.bindLong(_argIndex, time);
+ """.trimIndent()
+
+ @Test
+ fun aLongAndIntegerList() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE id IN(:ids) AND age > :time")
+ abstract List<Integer> selectAllIds(long time, List<Integer> ids);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`(collectionOut))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun aLongAndIntegerSet() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE id IN(:ids) AND age > :time")
+ abstract List<Integer> selectAllIds(long time, Set<Integer> ids);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`(collectionOut))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testMultipleBindParamsWithSameName() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE age > :age OR bage > :age")
+ abstract List<Integer> selectAllIds(int age);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`("""
+ final java.lang.String _sql = "SELECT id FROM users WHERE age > ? OR bage > ?";
+ final $QUERY _stmt = $QUERY.acquire(_sql, 2);
+ int _argIndex = 1;
+ _stmt.bindLong(_argIndex, age);
+ _argIndex = 2;
+ _stmt.bindLong(_argIndex, age);
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testMultipleBindParamsWithSameNameWithVarArg() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE age > :age OR bage > :age OR fage IN(:ages)")
+ abstract List<Integer> selectAllIds(int age, int... ages);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`("""
+ java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
+ _stringBuilder.append("SELECT id FROM users WHERE age > ");
+ _stringBuilder.append("?");
+ _stringBuilder.append(" OR bage > ");
+ _stringBuilder.append("?");
+ _stringBuilder.append(" OR fage IN(");
+ final int _inputSize = ages.length;
+ $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(")");
+ final java.lang.String _sql = _stringBuilder.toString();
+ final int _argCount = 2 + _inputSize;
+ final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ _stmt.bindLong(_argIndex, age);
+ _argIndex = 2;
+ _stmt.bindLong(_argIndex, age);
+ _argIndex = 3;
+ for (int _item : ages) {
+ _stmt.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testMultipleBindParamsWithSameNameWithVarArgInTwoBindings() {
+ singleQueryMethod("""
+ @Query("SELECT id FROM users WHERE age IN (:ages) OR bage > :age OR fage IN(:ages)")
+ abstract List<Integer> selectAllIds(int age, int... ages);
+ """) { writer ->
+ val scope = testCodeGenScope()
+ writer.prepareReadAndBind("_sql", "_stmt", scope)
+ assertThat(scope.generate().trim(), `is`("""
+ java.lang.StringBuilder _stringBuilder = $STRING_UTIL.newStringBuilder();
+ _stringBuilder.append("SELECT id FROM users WHERE age IN (");
+ final int _inputSize = ages.length;
+ $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(") OR bage > ");
+ _stringBuilder.append("?");
+ _stringBuilder.append(" OR fage IN(");
+ final int _inputSize_1 = ages.length;
+ $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize_1);
+ _stringBuilder.append(")");
+ final java.lang.String _sql = _stringBuilder.toString();
+ final int _argCount = 1 + _inputSize + _inputSize_1;
+ final $QUERY _stmt = $QUERY.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (int _item : ages) {
+ _stmt.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ _argIndex = 1 + _inputSize;
+ _stmt.bindLong(_argIndex, age);
+ _argIndex = 2 + _inputSize;
+ for (int _item_1 : ages) {
+ _stmt.bindLong(_argIndex, _item_1);
+ _argIndex ++;
+ }
+ """.trimIndent()))
+ }.compilesWithoutError()
+ }
+
+ fun singleQueryMethod(vararg input: String,
+ handler: (QueryWriter) -> Unit):
+ CompileTester {
+ return Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
+ .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+ ))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(Query::class, Dao::class)
+ .nextRunHandler { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.java)
+ .map {
+ Pair(it,
+ invocation.processingEnv.elementUtils
+ .getAllMembers(MoreElements.asType(it))
+ .filter {
+ MoreElements.isAnnotationPresent(it,
+ Query::class.java)
+ }
+ )
+ }.filter { it.second.isNotEmpty() }.first()
+ val parser = QueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = MoreTypes.asDeclared(owner.asType()),
+ executableElement = MoreElements.asExecutable(methods.first()))
+ val parsedQuery = parser.process()
+ handler(QueryWriter(parsedQuery))
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
new file mode 100644
index 0000000..d9904cb
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.testing
+
+import androidx.room.Query
+import com.google.common.truth.Truth
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourceSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.util.concurrent.atomic.AtomicBoolean
+
+@RunWith(JUnit4::class)
+class InProcessorTest {
+ @Test
+ fun testInProcessorTestRuns() {
+ val didRun = AtomicBoolean(false)
+ Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
+ .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
+ """
+ package foo.bar;
+ abstract public class MyClass {
+ @androidx.room.Query("foo")
+ abstract public void setFoo(String foo);
+ }
+ """))
+ .processedWith(TestProcessor.builder()
+ .nextRunHandler { invocation ->
+ didRun.set(true)
+ assertThat(invocation.annotations.size, `is`(1))
+ true
+ }
+ .forAnnotations(Query::class)
+ .build())
+ .compilesWithoutError()
+ assertThat(didRun.get(), `is`(true))
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/TestInvocation.kt b/room/compiler/src/test/kotlin/androidx/room/testing/TestInvocation.kt
new file mode 100644
index 0000000..9aff219
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/TestInvocation.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.testing
+
+import androidx.room.processor.Context
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.TypeElement
+
+data class TestInvocation(val processingEnv: ProcessingEnvironment,
+ val annotations: MutableSet<out TypeElement>,
+ val roundEnv: RoundEnvironment) {
+ val context = Context(processingEnv)
+
+ fun typeElement(qName: String): TypeElement {
+ return processingEnv.elementUtils.getTypeElement(qName)
+ }
+
+ val typeUtils by lazy { processingEnv.typeUtils }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/TestProcessor.kt b/room/compiler/src/test/kotlin/androidx/room/testing/TestProcessor.kt
new file mode 100644
index 0000000..2ec6294
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/TestProcessor.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.testing
+
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.RoundEnvironment
+import javax.annotation.processing.SupportedSourceVersion
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.TypeElement
+import kotlin.reflect.KClass
+
+@SupportedSourceVersion(SourceVersion.RELEASE_8)// test are compiled w/ J_8
+class TestProcessor(val handlers: List<(TestInvocation) -> Boolean>,
+ val annotations: MutableSet<String>) : AbstractProcessor() {
+ var count = 0
+ override fun process(
+ annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
+ return handlers.getOrNull(count++)?.invoke(
+ TestInvocation(processingEnv, annotations, roundEnv)) ?: true
+ }
+
+ override fun getSupportedAnnotationTypes(): MutableSet<String> {
+ return annotations
+ }
+
+ class Builder {
+ private var handlers = arrayListOf<(TestInvocation) -> Boolean>()
+ private var annotations = mutableSetOf<String>()
+ fun nextRunHandler(f: (TestInvocation) -> Boolean): Builder {
+ handlers.add(f)
+ return this
+ }
+
+ fun forAnnotations(vararg klasses: KClass<*>): Builder {
+ annotations.addAll(klasses.map { it.java.canonicalName })
+ return this
+ }
+
+ fun build(): TestProcessor {
+ if (annotations.isEmpty()) {
+ throw IllegalStateException("must provide at least 1 annotation")
+ }
+ if (handlers.isEmpty()) {
+ throw IllegalStateException("must provide at least 1 handler")
+ }
+ return TestProcessor(handlers, annotations)
+ }
+ }
+
+ companion object {
+ fun builder(): Builder = Builder()
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
new file mode 100644
index 0000000..345f94a
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import androidx.room.ColumnInfo
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Relation
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.ReactiveStreamsTypeNames
+import androidx.room.ext.RoomRxJava2TypeNames
+import androidx.room.ext.RxJava2TypeNames
+import androidx.room.processor.EntityProcessor
+import androidx.room.solver.CodeGenScope
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.verifier.DatabaseVerifier
+import androidx.room.writer.ClassWriter
+import com.google.auto.common.MoreElements
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.squareup.javapoet.ClassName
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import java.io.File
+import javax.lang.model.element.Element
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.tools.JavaFileObject
+
+object COMMON {
+ val USER by lazy {
+ loadJavaCode("common/input/User.java", "foo.bar.User")
+ }
+ val USER_TYPE_NAME by lazy {
+ ClassName.get("foo.bar", "User")
+ }
+ val BOOK by lazy {
+ loadJavaCode("common/input/Book.java", "foo.bar.Book")
+ }
+ val NOT_AN_ENTITY by lazy {
+ loadJavaCode("common/input/NotAnEntity.java", "foo.bar.NotAnEntity")
+ }
+
+ val NOT_AN_ENTITY_TYPE_NAME by lazy {
+ ClassName.get("foo.bar", "NotAnEntity")
+ }
+
+ val MULTI_PKEY_ENTITY by lazy {
+ loadJavaCode("common/input/MultiPKeyEntity.java", "MultiPKeyEntity")
+ }
+ val LIVE_DATA by lazy {
+ loadJavaCode("common/input/LiveData.java", LifecyclesTypeNames.LIVE_DATA.toString())
+ }
+ val COMPUTABLE_LIVE_DATA by lazy {
+ loadJavaCode("common/input/ComputableLiveData.java",
+ LifecyclesTypeNames.COMPUTABLE_LIVE_DATA.toString())
+ }
+ val PUBLISHER by lazy {
+ loadJavaCode("common/input/reactivestreams/Publisher.java",
+ ReactiveStreamsTypeNames.PUBLISHER.toString())
+ }
+ val FLOWABLE by lazy {
+ loadJavaCode("common/input/rxjava2/Flowable.java", RxJava2TypeNames.FLOWABLE.toString())
+ }
+
+ val RX2_ROOM by lazy {
+ loadJavaCode("common/input/Rx2Room.java", RoomRxJava2TypeNames.RX_ROOM.toString())
+ }
+
+ val DATA_SOURCE_FACTORY by lazy {
+ loadJavaCode("common/input/DataSource.java", "androidx.paging.DataSource")
+ }
+
+ val POSITIONAL_DATA_SOURCE by lazy {
+ loadJavaCode("common/input/PositionalDataSource.java",
+ PagingTypeNames.POSITIONAL_DATA_SOURCE.toString())
+ }
+}
+fun testCodeGenScope(): CodeGenScope {
+ return CodeGenScope(Mockito.mock(ClassWriter::class.java))
+}
+
+fun simpleRun(vararg jfos: JavaFileObject, f: (TestInvocation) -> Unit): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(jfos.toList() + JavaFileObjects.forSourceString("foo.bar.MyClass",
+ """
+ package foo.bar;
+ abstract public class MyClass {
+ @androidx.room.Query("foo")
+ abstract public void setFoo(String foo);
+ }
+ """))
+ .processedWith(TestProcessor.builder()
+ .nextRunHandler {
+ f(it)
+ true
+ }
+ .forAnnotations(Query::class, PrimaryKey::class, Embedded::class,
+ ColumnInfo::class, Relation::class, Entity::class)
+ .build())
+}
+
+fun loadJavaCode(fileName: String, qName: String): JavaFileObject {
+ val contents = File("src/test/data/$fileName").readText(Charsets.UTF_8)
+ return JavaFileObjects.forSourceString(qName, contents)
+}
+
+fun createVerifierFromEntities(invocation: TestInvocation): DatabaseVerifier {
+ val entities = invocation.roundEnv.getElementsAnnotatedWith(Entity::class.java).map {
+ EntityProcessor(invocation.context, MoreElements.asType(it)).process()
+ }
+ return DatabaseVerifier.create(invocation.context, Mockito.mock(Element::class.java),
+ entities)!!
+}
+
+/**
+ * Create mocks of [Element] and [TypeMirror] so that they can be used for instantiating a fake
+ * [androidx.room.vo.Field].
+ */
+fun mockElementAndType(): Pair<Element, TypeMirror> {
+ val element = mock(Element::class.java)
+ val type = mock(TypeMirror::class.java)
+ doReturn(TypeKind.DECLARED).`when`(type).kind
+ doReturn(type).`when`(element).asType()
+ return element to type
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
new file mode 100644
index 0000000..d72d52d
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -0,0 +1,273 @@
+/*
+ * 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 androidx.room.verifier
+
+import androidx.room.parser.Collate
+import androidx.room.parser.SQLTypeAffinity
+import androidx.room.processor.Context
+import androidx.room.testing.TestInvocation
+import androidx.room.vo.CallType
+import androidx.room.vo.Constructor
+import androidx.room.vo.Database
+import androidx.room.vo.Entity
+import androidx.room.vo.Field
+import androidx.room.vo.FieldGetter
+import androidx.room.vo.FieldSetter
+import androidx.room.vo.PrimaryKey
+import collect
+import columnNames
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.hasItem
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import simpleRun
+import java.sql.Connection
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.PrimitiveType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@RunWith(Parameterized::class)
+class DatabaseVerifierTest(private val useLocalizedCollation: Boolean) {
+ @Test
+ fun testSimpleDatabase() {
+ simpleRun { invocation ->
+ val verifier = createVerifier(invocation)
+ val stmt = verifier.connection.createStatement()
+ val rs = stmt.executeQuery("select * from sqlite_master WHERE type='table'")
+ assertThat(
+ rs.collect { set -> set.getString("name") }, hasItem(`is`("User")))
+ val table = verifier.connection.prepareStatement("select * from User")
+ assertThat(table.columnNames(), `is`(listOf("id", "name", "lastName", "ratio")))
+
+ assertThat(getPrimaryKeys(verifier.connection, "User"), `is`(listOf("id")))
+ }.compilesWithoutError()
+ }
+
+ private fun createVerifier(invocation: TestInvocation): DatabaseVerifier {
+ return DatabaseVerifier.create(invocation.context, mock(Element::class.java),
+ userDb(invocation.context).entities)!!
+ }
+
+ @Test
+ fun testFullEntityQuery() {
+ validQueryTest("select * from User") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ ColumnInfo("id", SQLTypeAffinity.INTEGER),
+ ColumnInfo("name", SQLTypeAffinity.TEXT),
+ ColumnInfo("lastName", SQLTypeAffinity.TEXT),
+ ColumnInfo("ratio", SQLTypeAffinity.REAL)
+ ))))
+ }
+ }
+
+ @Test
+ fun testPartialFields() {
+ validQueryTest("select id, lastName from User") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ ColumnInfo("id", SQLTypeAffinity.INTEGER),
+ ColumnInfo("lastName", SQLTypeAffinity.TEXT)
+ ))))
+ }
+ }
+
+ @Test
+ fun testRenamedField() {
+ validQueryTest("select id as myId, lastName from User") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ ColumnInfo("myId", SQLTypeAffinity.INTEGER),
+ ColumnInfo("lastName", SQLTypeAffinity.TEXT)
+ ))))
+ }
+ }
+
+ @Test
+ fun testGrouped() {
+ validQueryTest("select MAX(ratio) from User GROUP BY name") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ // unfortunately, we don't get this information
+ ColumnInfo("MAX(ratio)", SQLTypeAffinity.NULL)
+ ))))
+ }
+ }
+
+ @Test
+ fun testConcat() {
+ validQueryTest("select name || lastName as mergedName from User") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ // unfortunately, we don't get this information
+ ColumnInfo("mergedName", SQLTypeAffinity.NULL)
+ ))))
+ }
+ }
+
+ @Test
+ fun testResultWithArgs() {
+ validQueryTest("select id, name || lastName as mergedName from User where name LIKE ?") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ // unfortunately, we don't get this information
+ ColumnInfo("id", SQLTypeAffinity.INTEGER),
+ ColumnInfo("mergedName", SQLTypeAffinity.NULL)
+ ))))
+ }
+ }
+
+ @Test
+ fun testDeleteQuery() {
+ validQueryTest("delete from User where name LIKE ?") {
+ assertThat(it, `is`(QueryResultInfo(emptyList())))
+ }
+ }
+
+ @Test
+ fun testUpdateQuery() {
+ validQueryTest("update User set name = ? WHERE id = ?") {
+ assertThat(it, `is`(QueryResultInfo(emptyList())))
+ }
+ }
+
+ @Test
+ fun testBadQuery() {
+ simpleRun { invocation ->
+ val verifier = createVerifier(invocation)
+ val (_, error) = verifier.analyze("select foo from User")
+ assertThat(error, notNullValue())
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun testCollate() {
+ validQueryTest("SELECT id, name FROM user ORDER BY name COLLATE LOCALIZED ASC") {
+ assertThat(it, `is`(
+ QueryResultInfo(listOf(
+ // unfortunately, we don't get this information
+ ColumnInfo("id", SQLTypeAffinity.INTEGER),
+ ColumnInfo("name", SQLTypeAffinity.TEXT)
+ ))))
+ }
+ }
+
+ @Test
+ fun testCollateBasQuery() {
+ simpleRun { invocation ->
+ val verifier = createVerifier(invocation)
+ val (_, error) = verifier.analyze(
+ "SELECT id, name FROM user ORDER BY name COLLATE LOCALIZEDASC")
+ assertThat(error, notNullValue())
+ }.compilesWithoutError()
+ }
+
+ private fun validQueryTest(sql: String, cb: (QueryResultInfo) -> Unit) {
+ simpleRun { invocation ->
+ val verifier = createVerifier(invocation)
+ val info = verifier.analyze(sql)
+ cb(info)
+ }.compilesWithoutError()
+ }
+
+ private fun userDb(context: Context): Database {
+ return database(entity("User",
+ field("id", primitive(context, TypeKind.INT), SQLTypeAffinity.INTEGER),
+ field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
+ field("lastName", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
+ field("ratio", primitive(context, TypeKind.FLOAT), SQLTypeAffinity.REAL)))
+ }
+
+ private fun database(vararg entities: Entity): Database {
+ return Database(
+ element = mock(TypeElement::class.java),
+ type = mock(TypeMirror::class.java),
+ entities = entities.toList(),
+ daoMethods = emptyList(),
+ version = -1,
+ exportSchema = false,
+ enableForeignKeys = false)
+ }
+
+ private fun entity(tableName: String, vararg fields: Field): Entity {
+ return Entity(
+ element = mock(TypeElement::class.java),
+ tableName = tableName,
+ type = mock(DeclaredType::class.java),
+ fields = fields.toList(),
+ embeddedFields = emptyList(),
+ indices = emptyList(),
+ primaryKey = PrimaryKey(null, fields.take(1), false),
+ foreignKeys = emptyList(),
+ constructor = Constructor(mock(ExecutableElement::class.java), emptyList())
+ )
+ }
+
+ private fun field(name: String, type: TypeMirror, affinity: SQLTypeAffinity): Field {
+ val element = mock(Element::class.java)
+ doReturn(type).`when`(element).asType()
+ val f = Field(
+ element = element,
+ name = name,
+ type = type,
+ columnName = name,
+ affinity = affinity,
+ collate = if (useLocalizedCollation && affinity == SQLTypeAffinity.TEXT) {
+ Collate.LOCALIZED
+ } else {
+ null
+ }
+ )
+ assignGetterSetter(f, name, type)
+ return f
+ }
+
+ private fun assignGetterSetter(f: Field, name: String, type: TypeMirror) {
+ f.getter = FieldGetter(name, type, CallType.FIELD)
+ f.setter = FieldSetter(name, type, CallType.FIELD)
+ }
+
+ private fun primitive(context: Context, kind: TypeKind): PrimitiveType {
+ return context.processingEnv.typeUtils.getPrimitiveType(kind)
+ }
+
+ private fun getPrimaryKeys(connection: Connection, tableName: String): List<String> {
+ val stmt = connection.createStatement()
+ val resultSet = stmt.executeQuery("PRAGMA table_info($tableName)")
+ return resultSet.collect {
+ Pair(it.getString("name"), it.getInt("pk"))
+ }
+ .filter { it.second > 0 }
+ .sortedBy { it.second }
+ .map { it.first }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "useLocalizedCollation={0}")
+ @JvmStatic
+ fun params() = arrayListOf(true, false)
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/vo/EntityTest.kt b/room/compiler/src/test/kotlin/androidx/room/vo/EntityTest.kt
new file mode 100644
index 0000000..627e659
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/vo/EntityTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018 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 androidx.room.vo
+
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.DeclaredType
+
+@RunWith(JUnit4::class)
+class EntityTest {
+
+ @Test
+ fun shouldBeDeletedAfter() {
+ val child = createEntity("Child", listOf(
+ createForeignKey("NoAction", ForeignKeyAction.NO_ACTION, false),
+ createForeignKey("NoActionDeferred", ForeignKeyAction.NO_ACTION, true),
+ createForeignKey("Restrict", ForeignKeyAction.RESTRICT, false),
+ createForeignKey("RestrictDeferred", ForeignKeyAction.RESTRICT, true),
+ createForeignKey("SetNull", ForeignKeyAction.SET_NULL, false),
+ createForeignKey("SetNullDeferred", ForeignKeyAction.SET_NULL, true),
+ createForeignKey("SetDefault", ForeignKeyAction.SET_DEFAULT, false),
+ createForeignKey("SetDefaultDeferred", ForeignKeyAction.SET_DEFAULT, true),
+ createForeignKey("Cascade", ForeignKeyAction.CASCADE, false),
+ createForeignKey("CascadeDeferred", ForeignKeyAction.CASCADE, true)))
+ val noAction = createEntity("NoAction")
+ val noActionDeferred = createEntity("NoActionDeferred")
+ val restrict = createEntity("Restrict")
+ val restrictDeferred = createEntity("RestrictDeferred")
+ val setNull = createEntity("SetNull")
+ val setNullDeferred = createEntity("SetNullDeferred")
+ val setDefault = createEntity("SetDefault")
+ val setDefaultDeferred = createEntity("SetDefaultDeferred")
+ val cascade = createEntity("Cascade")
+ val cascadeDeferred = createEntity("CascadeDeferred")
+ val irrelevant = createEntity("Irrelevant")
+ assertThat(child.shouldBeDeletedAfter(noAction), `is`(true))
+ assertThat(child.shouldBeDeletedAfter(noActionDeferred), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(restrict), `is`(true))
+ assertThat(child.shouldBeDeletedAfter(restrictDeferred), `is`(true))
+ assertThat(child.shouldBeDeletedAfter(setNull), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(setNullDeferred), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(setDefault), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(setDefaultDeferred), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(cascade), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(cascadeDeferred), `is`(false))
+ assertThat(child.shouldBeDeletedAfter(irrelevant), `is`(false))
+ }
+
+ private fun createEntity(
+ tableName: String,
+ foreignKeys: List<ForeignKey> = emptyList()): Entity {
+ return Entity(
+ element = mock(TypeElement::class.java),
+ tableName = tableName,
+ type = mock(DeclaredType::class.java),
+ fields = emptyList(),
+ embeddedFields = emptyList(),
+ primaryKey = PrimaryKey(mock(Element::class.java), emptyList(), false),
+ indices = emptyList(),
+ foreignKeys = foreignKeys,
+ constructor = Constructor(mock(ExecutableElement::class.java), emptyList()))
+ }
+
+ private fun createForeignKey(
+ parentTable: String,
+ onDelete: ForeignKeyAction,
+ deferred: Boolean): ForeignKey {
+ return ForeignKey(
+ parentTable = parentTable,
+ parentColumns = emptyList(),
+ childFields = emptyList(),
+ onDelete = onDelete,
+ onUpdate = ForeignKeyAction.NO_ACTION,
+ deferred = deferred)
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/vo/IndexTest.kt b/room/compiler/src/test/kotlin/androidx/room/vo/IndexTest.kt
new file mode 100644
index 0000000..e7150ea
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/vo/IndexTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 androidx.room.vo
+
+import androidx.room.parser.SQLTypeAffinity
+import mockElementAndType
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class IndexTest {
+ @Test
+ fun createSimpleSQL() {
+ val index = Index("foo", false, listOf(mockField("bar"), mockField("baz")))
+ MatcherAssert.assertThat(index.createQuery("my_table"), CoreMatchers.`is`(
+ "CREATE INDEX `foo` ON `my_table` (`bar`, `baz`)"
+ ))
+ }
+
+ @Test
+ fun createUnique() {
+ val index = Index("foo", true, listOf(mockField("bar"), mockField("baz")))
+ MatcherAssert.assertThat(index.createQuery("my_table"), CoreMatchers.`is`(
+ "CREATE UNIQUE INDEX `foo` ON `my_table` (`bar`, `baz`)"
+ ))
+ }
+
+ private fun mockField(columnName: String): Field {
+ val (element, type) = mockElementAndType()
+ return Field(
+ element = element,
+ name = columnName + "_field",
+ affinity = SQLTypeAffinity.TEXT,
+ type = type,
+ columnName = columnName
+ )
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
new file mode 100644
index 0000000..5ed7840
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import COMMON
+import androidx.room.ext.RoomTypeNames
+import androidx.room.processor.DaoProcessor
+import androidx.room.testing.TestProcessor
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import createVerifierFromEntities
+import loadJavaCode
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.tools.JavaFileObject
+
+@RunWith(JUnit4::class)
+class DaoWriterTest {
+ @Test
+ fun complexDao() {
+ singleDao(
+ loadJavaCode("databasewriter/input/ComplexDatabase.java",
+ "foo.bar.ComplexDatabase"),
+ loadJavaCode("daoWriter/input/ComplexDao.java", "foo.bar.ComplexDao")
+ ).compilesWithoutError().and().generatesSources(
+ loadJavaCode("daoWriter/output/ComplexDao.java", "foo.bar.ComplexDao_Impl")
+ )
+ }
+
+ @Test
+ fun writerDao() {
+ singleDao(
+ loadJavaCode("daoWriter/input/WriterDao.java", "foo.bar.WriterDao")
+ ).compilesWithoutError().and().generatesSources(
+ loadJavaCode("daoWriter/output/WriterDao.java", "foo.bar.WriterDao_Impl")
+ )
+ }
+
+ @Test
+ fun deletionDao() {
+ singleDao(
+ loadJavaCode("daoWriter/input/DeletionDao.java", "foo.bar.DeletionDao")
+ ).compilesWithoutError().and().generatesSources(
+ loadJavaCode("daoWriter/output/DeletionDao.java", "foo.bar.DeletionDao_Impl")
+ )
+ }
+
+ @Test
+ fun updateDao() {
+ singleDao(
+ loadJavaCode("daoWriter/input/UpdateDao.java", "foo.bar.UpdateDao")
+ ).compilesWithoutError().and().generatesSources(
+ loadJavaCode("daoWriter/output/UpdateDao.java", "foo.bar.UpdateDao_Impl")
+ )
+ }
+
+ fun singleDao(vararg jfo: JavaFileObject): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(jfo.toList() + COMMON.USER + COMMON.MULTI_PKEY_ENTITY + COMMON.BOOK +
+ COMMON.LIVE_DATA + COMMON.COMPUTABLE_LIVE_DATA)
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(androidx.room.Dao::class)
+ .nextRunHandler { invocation ->
+ val dao = invocation.roundEnv
+ .getElementsAnnotatedWith(
+ androidx.room.Dao::class.java)
+ .first()
+ val db = invocation.roundEnv
+ .getElementsAnnotatedWith(
+ androidx.room.Database::class.java)
+ .firstOrNull()
+ val dbType = MoreTypes.asDeclared(if (db != null) {
+ db.asType()
+ } else {
+ invocation.context.processingEnv.elementUtils
+ .getTypeElement(RoomTypeNames.ROOM_DB.toString()).asType()
+ })
+ val parser = DaoProcessor(
+ baseContext = invocation.context,
+ element = MoreElements.asType(dao),
+ dbType = dbType,
+ dbVerifier = createVerifierFromEntities(invocation))
+ val parsedDao = parser.process()
+ DaoWriter(parsedDao, invocation.processingEnv)
+ .write(invocation.processingEnv)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
new file mode 100644
index 0000000..f217e63
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import COMMON
+import androidx.room.RoomProcessor
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import loadJavaCode
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.tools.JavaFileObject
+
+@RunWith(JUnit4::class)
+class DatabaseWriterTest {
+ @Test
+ fun simpleDb() {
+ singleDb(
+ loadJavaCode("databasewriter/input/ComplexDatabase.java",
+ "foo.bar.ComplexDatabase"),
+ loadJavaCode("daoWriter/input/ComplexDao.java",
+ "foo.bar.ComplexDao")
+ ).compilesWithoutError().and().generatesSources(
+ loadJavaCode("databasewriter/output/ComplexDatabase.java",
+ "foo.bar.ComplexDatabase_Impl")
+ )
+ }
+
+ private fun singleDb(vararg jfo: JavaFileObject): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(jfo.toList() + COMMON.USER + COMMON.LIVE_DATA + COMMON.COMPUTABLE_LIVE_DATA)
+ .processedWith(RoomProcessor())
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/EntityCursorConverterWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/EntityCursorConverterWriterTest.kt
new file mode 100644
index 0000000..525cfe3
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/EntityCursorConverterWriterTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.room.processor.BaseEntityParserTest
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeSpec
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.lang.model.element.Modifier
+
+@RunWith(JUnit4::class)
+class EntityCursorConverterWriterTest : BaseEntityParserTest() {
+ companion object {
+ val OUT_PREFIX = """
+ package foo.bar;
+ import android.database.Cursor;
+ import java.lang.SuppressWarnings;
+ import javax.annotation.Generated;
+ @Generated("androidx.room.RoomProcessor")
+ @SuppressWarnings("unchecked")
+ public class MyContainerClass {
+ """.trimIndent()
+ const val OUT_SUFFIX = "}"
+ }
+
+ @Test
+ fun generateSimple() {
+ generateAndMatch(
+ """
+ @PrimaryKey
+ private int id;
+ String name;
+ String lastName;
+ int age;
+ public int getId() { return id; }
+ public void setId(int id) { this.id = id; }
+ """,
+ """
+ private MyEntity __entityCursorConverter_fooBarMyEntity(Cursor cursor) {
+ final MyEntity _entity;
+ final int _cursorIndexOfId = cursor.getColumnIndex("id");
+ final int _cursorIndexOfName = cursor.getColumnIndex("name");
+ final int _cursorIndexOfLastName = cursor.getColumnIndex("lastName");
+ final int _cursorIndexOfAge = cursor.getColumnIndex("age");
+ _entity = new MyEntity();
+ if (_cursorIndexOfId != -1) {
+ final int _tmpId;
+ _tmpId = cursor.getInt(_cursorIndexOfId);
+ _entity.setId(_tmpId);
+ }
+ if (_cursorIndexOfName != -1) {
+ _entity.name = cursor.getString(_cursorIndexOfName);
+ }
+ if (_cursorIndexOfLastName != -1) {
+ _entity.lastName = cursor.getString(_cursorIndexOfLastName);
+ }
+ if (_cursorIndexOfAge != -1) {
+ _entity.age = cursor.getInt(_cursorIndexOfAge);
+ }
+ return _entity;
+ }
+ """.trimIndent())
+ }
+
+ fun generateAndMatch(input: String, output: String,
+ attributes: Map<String, String> = mapOf()) {
+ generate(input, attributes)
+ .compilesWithoutError()
+ .and()
+ .generatesSources(JavaFileObjects.forSourceString(
+ "foo.bar.MyEntity_CursorConverter",
+ listOf(OUT_PREFIX, output, OUT_SUFFIX).joinToString("\n")))
+ }
+
+ fun generate(input: String, attributes: Map<String, String> = mapOf()): CompileTester {
+ return singleEntity(input, attributes) { entity, invocation ->
+ val className = ClassName.get("foo.bar", "MyContainerClass")
+ val writer = object : ClassWriter(className) {
+ override fun createTypeSpecBuilder(): TypeSpec.Builder {
+ getOrCreateMethod(EntityCursorConverterWriter(entity))
+ return TypeSpec.classBuilder(className).apply {
+ addModifiers(Modifier.PUBLIC)
+ }
+ }
+ }
+ writer.write(invocation.processingEnv)
+ }
+ }
+}
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
new file mode 100644
index 0000000..0ad0efc
--- /dev/null
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.writer
+
+import androidx.annotation.NonNull
+import androidx.room.processor.DatabaseProcessor
+import androidx.room.testing.TestInvocation
+import androidx.room.testing.TestProcessor
+import androidx.room.vo.Database
+import com.google.auto.common.MoreElements
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class SQLiteOpenHelperWriterTest {
+ companion object {
+ const val ENTITY_PREFIX = """
+ package foo.bar;
+ import androidx.annotation.NonNull;
+ import androidx.room.*;
+ @Entity%s
+ public class MyEntity {
+ """
+ const val ENTITY_SUFFIX = "}"
+ const val DATABASE_CODE = """
+ package foo.bar;
+ import androidx.room.*;
+ @Database(entities = {MyEntity.class}, version = 3)
+ abstract public class MyDatabase extends RoomDatabase {
+ }
+ """
+ }
+
+ @Test
+ fun createSimpleEntity() {
+ singleEntity(
+ """
+ @PrimaryKey
+ @NonNull
+ String uuid;
+ String name;
+ int age;
+ """.trimIndent()
+ ) { database, _ ->
+ val query = SQLiteOpenHelperWriter(database)
+ .createQuery(database.entities.first())
+ assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
+ " `MyEntity` (`uuid` TEXT NOT NULL, `name` TEXT, `age` INTEGER NOT NULL," +
+ " PRIMARY KEY(`uuid`))"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun multiplePrimaryKeys() {
+ singleEntity(
+ """
+ @NonNull
+ String uuid;
+ @NonNull
+ String name;
+ int age;
+ """.trimIndent(), attributes = mapOf("primaryKeys" to "{\"uuid\", \"name\"}")
+ ) { database, _ ->
+ val query = SQLiteOpenHelperWriter(database)
+ .createQuery(database.entities.first())
+ assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
+ " `MyEntity` (`uuid` TEXT NOT NULL, `name` TEXT NOT NULL, " +
+ "`age` INTEGER NOT NULL, PRIMARY KEY(`uuid`, `name`))"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun autoIncrementObject() {
+ listOf("Long", "Integer").forEach { type ->
+ singleEntity(
+ """
+ @PrimaryKey(autoGenerate = true)
+ $type uuid;
+ String name;
+ int age;
+ """.trimIndent()
+ ) { database, _ ->
+ val query = SQLiteOpenHelperWriter(database)
+ .createQuery(database.entities.first())
+ assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
+ " `MyEntity` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT," +
+ " `name` TEXT, `age` INTEGER NOT NULL)"))
+ }.compilesWithoutError()
+ }
+ }
+
+ @Test
+ fun autoIncrementPrimitives() {
+ listOf("long", "int").forEach { type ->
+ singleEntity(
+ """
+ @PrimaryKey(autoGenerate = true)
+ $type uuid;
+ String name;
+ int age;
+ """.trimIndent()
+ ) { database, _ ->
+ val query = SQLiteOpenHelperWriter(database)
+ .createQuery(database.entities.first())
+ assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
+ " `MyEntity` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
+ " `name` TEXT, `age` INTEGER NOT NULL)"))
+ }.compilesWithoutError()
+ }
+ }
+
+ fun singleEntity(input: String, attributes: Map<String, String> = mapOf(),
+ handler: (Database, TestInvocation) -> Unit): CompileTester {
+ val attributesReplacement: String
+ if (attributes.isEmpty()) {
+ attributesReplacement = ""
+ } else {
+ attributesReplacement = "(" +
+ attributes.entries.map { "${it.key} = ${it.value}" }.joinToString(",") +
+ ")".trimIndent()
+ }
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
+ ENTITY_PREFIX.format(attributesReplacement) + input + ENTITY_SUFFIX
+ ), JavaFileObjects.forSourceString("foo.bar.MyDatabase",
+ DATABASE_CODE)))
+ .processedWith(TestProcessor.builder()
+ .forAnnotations(androidx.room.Database::class,
+ NonNull::class)
+ .nextRunHandler { invocation ->
+ val db = MoreElements.asType(invocation.roundEnv
+ .getElementsAnnotatedWith(
+ androidx.room.Database::class.java)
+ .first())
+ handler(DatabaseProcessor(invocation.context, db).process(), invocation)
+ true
+ }
+ .build())
+ }
+}
diff --git a/room/guava/build.gradle b/room/guava/build.gradle
index 3837406..3fc535c 100644
--- a/room/guava/build.gradle
+++ b/room/guava/build.gradle
@@ -24,9 +24,9 @@
}
dependencies {
- api(project(":room:common"))
- api(project(":room:runtime"))
- api(project(":arch:runtime"))
+ api(project(":room:room-common"))
+ api(project(":room:room-runtime"))
+ api(project(":arch:core-runtime"))
api(SUPPORT_ANNOTATIONS, libs.support_exclude_config)
api(GUAVA_ANDROID)
}
diff --git a/room/guava/src/main/AndroidManifest.xml b/room/guava/src/main/AndroidManifest.xml
index 7c5004a..cba56a8 100644
--- a/room/guava/src/main/AndroidManifest.xml
+++ b/room/guava/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.room.guava">
+ package="androidx.room.guava">
</manifest>
diff --git a/room/guava/src/main/java/android/arch/persistence/room/guava/GuavaRoom.java b/room/guava/src/main/java/android/arch/persistence/room/guava/GuavaRoom.java
deleted file mode 100644
index 1c9a488..0000000
--- a/room/guava/src/main/java/android/arch/persistence/room/guava/GuavaRoom.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2018 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.guava;
-
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.persistence.room.RoomSQLiteQuery;
-import android.support.annotation.RestrictTo;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListenableFutureTask;
-
-import java.util.concurrent.Callable;
-
-/**
- * A class to hold static methods used by code generation in Room's Guava compatibility library.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@SuppressWarnings("unused") // Used in GuavaListenableFutureQueryResultBinder code generation.
-public class GuavaRoom {
-
- private GuavaRoom() {}
-
- /**
- * Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
- * {@link ArchTaskExecutor}'s background-threaded Executor.
- */
- public static <T> ListenableFuture<T> createListenableFuture(
- final Callable<T> callable,
- final RoomSQLiteQuery query,
- final boolean releaseQuery) {
- ListenableFutureTask<T> listenableFutureTask = ListenableFutureTask.create(callable);
- ArchTaskExecutor.getInstance().executeOnDiskIO(listenableFutureTask);
-
- if (releaseQuery) {
- Futures.addCallback(
- listenableFutureTask,
- new FutureCallback<T>() {
- @Override
- public void onSuccess(T t) {
- query.release();
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- query.release();
- }
- },
- directExecutor());
- }
-
- return listenableFutureTask;
- }
-}
diff --git a/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java b/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
new file mode 100644
index 0000000..be483aa
--- /dev/null
+++ b/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 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 androidx.room.guava;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import androidx.annotation.RestrictTo;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.room.RoomSQLiteQuery;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListenableFutureTask;
+
+import java.util.concurrent.Callable;
+
+/**
+ * A class to hold static methods used by code generation in Room's Guava compatibility library.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressWarnings("unused") // Used in GuavaListenableFutureQueryResultBinder code generation.
+public class GuavaRoom {
+
+ private GuavaRoom() {}
+
+ /**
+ * Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
+ * {@link ArchTaskExecutor}'s background-threaded Executor.
+ */
+ public static <T> ListenableFuture<T> createListenableFuture(
+ final Callable<T> callable,
+ final RoomSQLiteQuery query,
+ final boolean releaseQuery) {
+ ListenableFutureTask<T> listenableFutureTask = ListenableFutureTask.create(callable);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(listenableFutureTask);
+
+ if (releaseQuery) {
+ Futures.addCallback(
+ listenableFutureTask,
+ new FutureCallback<T>() {
+ @Override
+ public void onSuccess(T t) {
+ query.release();
+ }
+
+ @Override
+ public void onFailure(Throwable throwable) {
+ query.release();
+ }
+ },
+ directExecutor());
+ }
+
+ return listenableFutureTask;
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/build.gradle b/room/integration-tests/kotlintestapp/build.gradle
index 162f325..e32db0c 100644
--- a/room/integration-tests/kotlintestapp/build.gradle
+++ b/room/integration-tests/kotlintestapp/build.gradle
@@ -39,13 +39,13 @@
}
dependencies {
- implementation(project(":room:common"))
- implementation(project(":persistence:db"))
- implementation(project(":persistence:db-framework"))
- implementation(project(":room:runtime"))
- implementation(project(":arch:runtime"))
- implementation(project(":lifecycle:extensions"))
- kaptAndroidTest project(":room:compiler")
+ implementation(project(":room:room-common"))
+ implementation(project(":sqlite:sqlite"))
+ implementation(project(":sqlite:sqlite-framework"))
+ implementation(project(":room:room-runtime"))
+ implementation(project(":arch:core-runtime"))
+ implementation(project(":lifecycle:lifecycle-extensions"))
+ kaptAndroidTest project(":room:room-compiler")
androidTestImplementation(TEST_RUNNER) {
exclude module: 'support-annotations'
@@ -56,12 +56,12 @@
exclude module: "hamcrest-core"
})
// IJ's gradle integration just cannot figure this out ...
- androidTestImplementation project(':lifecycle:extensions')
- androidTestImplementation project(':lifecycle:common')
- androidTestImplementation project(':lifecycle:runtime')
- androidTestImplementation project(':room:guava')
- androidTestImplementation project(':room:testing')
- androidTestImplementation project(':room:rxjava2')
+ androidTestImplementation project(':lifecycle:lifecycle-extensions')
+ androidTestImplementation project(':lifecycle:lifecycle-common')
+ androidTestImplementation project(':lifecycle:lifecycle-runtime')
+ androidTestImplementation project(':room:room-guava')
+ androidTestImplementation project(':room:room-testing')
+ androidTestImplementation project(':room:room-rxjava2')
androidTestImplementation project(':arch:core-testing')
androidTestImplementation(GUAVA_ANDROID)
androidTestImplementation(RX_JAVA)
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.BooksDatabase/1.json b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.BooksDatabase/1.json
deleted file mode 100644
index dd056e2..0000000
--- a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.BooksDatabase/1.json
+++ /dev/null
@@ -1,159 +0,0 @@
-{
- "formatVersion": 1,
- "database": {
- "version": 1,
- "identityHash": "64fa560604c57044726b190dadbd8258",
- "entities": [
- {
- "tableName": "Book",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `title` TEXT NOT NULL, `bookPublisherId` TEXT NOT NULL, PRIMARY KEY(`bookId`), FOREIGN KEY(`bookPublisherId`) REFERENCES `Publisher`(`publisherId`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
- "fields": [
- {
- "fieldPath": "bookId",
- "columnName": "bookId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "title",
- "columnName": "title",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "bookPublisherId",
- "columnName": "bookPublisherId",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "bookId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "Publisher",
- "onDelete": "NO ACTION",
- "onUpdate": "NO ACTION",
- "columns": [
- "bookPublisherId"
- ],
- "referencedColumns": [
- "publisherId"
- ]
- }
- ]
- },
- {
- "tableName": "Author",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`authorId` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`authorId`))",
- "fields": [
- {
- "fieldPath": "authorId",
- "columnName": "authorId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "authorId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "Publisher",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`publisherId` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`publisherId`))",
- "fields": [
- {
- "fieldPath": "publisherId",
- "columnName": "publisherId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "publisherId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "BookAuthor",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `authorId` TEXT NOT NULL, PRIMARY KEY(`bookId`, `authorId`), FOREIGN KEY(`bookId`) REFERENCES `Book`(`bookId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`authorId`) REFERENCES `Author`(`authorId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
- "fields": [
- {
- "fieldPath": "bookId",
- "columnName": "bookId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "authorId",
- "columnName": "authorId",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "bookId",
- "authorId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "Book",
- "onDelete": "CASCADE",
- "onUpdate": "CASCADE",
- "columns": [
- "bookId"
- ],
- "referencedColumns": [
- "bookId"
- ]
- },
- {
- "table": "Author",
- "onDelete": "CASCADE",
- "onUpdate": "CASCADE",
- "columns": [
- "authorId"
- ],
- "referencedColumns": [
- "authorId"
- ]
- }
- ]
- }
- ],
- "setupQueries": [
- "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"64fa560604c57044726b190dadbd8258\")"
- ]
- }
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.TestDatabase/1.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.TestDatabase/1.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/1.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/1.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/1.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/1.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/2.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/2.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/2.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/2.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/3.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/3.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/3.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/3.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/4.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/4.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/4.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/4.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/5.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/5.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/5.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/5.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/6.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/6.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/6.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/6.json
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/7.json b/room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/7.json
similarity index 100%
rename from room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.migration.MigrationDbKotlin/7.json
rename to room/integration-tests/kotlintestapp/schemas/androidx.room.integration.kotlintestapp.migration.MigrationDbKotlin/7.json
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt
deleted file mode 100644
index 33162ee..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/TestDatabase.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.kotlintestapp
-
-import android.arch.persistence.room.Database
-import android.arch.persistence.room.RoomDatabase
-import android.arch.persistence.room.integration.kotlintestapp.dao.BooksDao
-import android.arch.persistence.room.integration.kotlintestapp.dao.DependencyDao
-import android.arch.persistence.room.integration.kotlintestapp.dao.DerivedDao
-import android.arch.persistence.room.integration.kotlintestapp.vo.Author
-import android.arch.persistence.room.integration.kotlintestapp.vo.Book
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
-import android.arch.persistence.room.integration.kotlintestapp.vo.DataClassFromDependency
-import android.arch.persistence.room.integration.kotlintestapp.vo.NoArgClass
-import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
-
-@Database(entities = [Book::class, Author::class, Publisher::class, BookAuthor::class,
- NoArgClass::class, DataClassFromDependency::class], version = 1)
-abstract class TestDatabase : RoomDatabase() {
-
- abstract fun booksDao(): BooksDao
-
- abstract fun derivedDao(): DerivedDao
-
- abstract fun dependencyDao(): DependencyDao
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt
deleted file mode 100644
index a80e840..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BaseDao.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.kotlintestapp.dao
-
-import android.arch.persistence.room.Delete
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.OnConflictStrategy
-import android.arch.persistence.room.Update
-
-interface BaseDao<T> {
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(t: T)
-
- @Insert
- fun insertAll(t: List<T>)
-
- @Insert
- fun insertAllArg(vararg t: T)
-
- @Update
- fun update(t: T)
-
- @Delete
- fun delete(t: T)
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
deleted file mode 100644
index a3f3934..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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.kotlintestapp.dao
-
-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.Transaction
-import android.arch.persistence.room.TypeConverters
-import android.arch.persistence.room.integration.kotlintestapp.vo.Author
-import android.arch.persistence.room.integration.kotlintestapp.vo.Book
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
-import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
-import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
-import android.arch.persistence.room.integration.kotlintestapp.vo.PublisherWithBookSales
-import android.arch.persistence.room.integration.kotlintestapp.vo.PublisherWithBooks
-import com.google.common.base.Optional
-import com.google.common.util.concurrent.ListenableFuture
-import io.reactivex.Flowable
-import io.reactivex.Maybe
-import io.reactivex.Single
-
-@Dao
-interface BooksDao {
-
- @Insert
- fun addPublishers(vararg publishers: Publisher)
-
- @Delete
- fun deletePublishers(vararg publishers: Publisher)
-
- @Insert
- fun addAuthors(vararg authors: Author)
-
- @Query("SELECT * FROM author WHERE authorId = :authorId")
- fun getAuthor(authorId: String): Author
-
- @Insert
- fun addBooks(vararg books: Book)
-
- @Insert
- fun addBookAuthors(vararg bookAuthors: BookAuthor)
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBook(bookId: String): Book
-
- @Query("""SELECT * FROM book WHERE
- bookId IN(:bookIds)
- order by bookId DESC""")
- fun getBooksMultiLineQuery(bookIds: List<String>): List<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookLiveData(bookId: String): LiveData<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookFlowable(bookId: String): Flowable<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookJavaOptional(bookId: String): java.util.Optional<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookListenableFuture(bookId: String): ListenableFuture<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookOptional(bookId: String): Optional<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookOptionalFlowable(bookId: String): Flowable<Optional<Book>>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookOptionalListenableFuture(bookId: String): ListenableFuture<Optional<Book>>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookSingle(bookId: String): Single<Book>
-
- @Query("SELECT * FROM book WHERE bookId = :bookId")
- fun getBookMaybe(bookId: String): Maybe<Book>
-
- @Query("SELECT * FROM book INNER JOIN publisher " +
- "ON book.bookPublisherId = publisher.publisherId ")
- fun getBooksWithPublisher(): List<BookWithPublisher>
-
- @Query("SELECT * FROM book INNER JOIN publisher " +
- "ON book.bookPublisherId = publisher.publisherId ")
- fun getBooksWithPublisherLiveData(): LiveData<List<BookWithPublisher>>
-
- @Query("SELECT * FROM book INNER JOIN publisher " +
- "ON book.bookPublisherId = publisher.publisherId ")
- fun getBooksWithPublisherFlowable(): Flowable<List<BookWithPublisher>>
-
- @Query("SELECT * FROM book INNER JOIN publisher " +
- "ON book.bookPublisherId = publisher.publisherId ")
- fun getBooksWithPublisherListenableFuture(): ListenableFuture<List<BookWithPublisher>>
-
- @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
- fun getPublisherWithBooks(publisherId: String): PublisherWithBooks
-
- @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
- fun getPublisherWithBookSales(publisherId: String): PublisherWithBookSales
-
- @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
- fun getPublisherWithBooksLiveData(publisherId: String): LiveData<PublisherWithBooks>
-
- @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
- fun getPublisherWithBooksFlowable(publisherId: String): Flowable<PublisherWithBooks>
-
- @Query("UPDATE book SET title = :title WHERE bookId = :bookId")
- fun updateBookTitle(bookId: String, title: String?)
-
- @Query("SELECT * FROM book WHERE languages & :langs != 0 ORDER BY bookId ASC")
- @TypeConverters(Lang::class)
- fun findByLanguages(langs: Set<Lang>): List<Book>
-
- @Transaction
- fun deleteAndAddPublisher(oldPublisher: Publisher, newPublisher: Publisher,
- fail: Boolean = false) {
- deletePublishers(oldPublisher)
- if (fail) {
- throw RuntimeException()
- }
- addPublishers(newPublisher)
- }
-
- @Query("SELECT * FROM Publisher")
- fun getPublishers(): List<Publisher>
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/DerivedDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/DerivedDao.kt
deleted file mode 100644
index 5245a01..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/DerivedDao.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.kotlintestapp.dao
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.integration.kotlintestapp.vo.Author
-
-@Dao
-interface DerivedDao : BaseDao<Author> {
-
- @Query("SELECT * FROM author WHERE authorId = :authorId")
- fun getAuthor(authorId: String): Author
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationDbKotlin.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationDbKotlin.kt
deleted file mode 100644
index 67374a5..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationDbKotlin.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.kotlintestapp.migration
-
-import android.arch.persistence.db.SupportSQLiteDatabase
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Database
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.ForeignKey
-import android.arch.persistence.room.Ignore
-import android.arch.persistence.room.Index
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.RoomDatabase
-import android.content.ContentValues
-import android.database.sqlite.SQLiteDatabase
-
-@Database(version = MigrationDbKotlin.LATEST_VERSION,
- entities = arrayOf(MigrationDbKotlin.Entity1::class, MigrationDbKotlin.Entity2::class,
- MigrationDbKotlin.Entity4::class))
-abstract class MigrationDbKotlin : RoomDatabase() {
-
- internal abstract fun dao(): MigrationDao
-
- @Entity(indices = arrayOf(Index(value = "name", unique = true)))
- data class Entity1(@PrimaryKey var id: Int = 0, var name: String?) {
-
- companion object {
- val TABLE_NAME = "Entity1"
- }
- }
-
- @Entity
- open class Entity2(@PrimaryKey var id: Int = 0, var addedInV3: String?, var name: String?) {
- companion object {
- val TABLE_NAME = "Entity2"
- }
- }
-
- @Entity
- data class Entity3(@PrimaryKey var id: Int = 0, @Ignore var removedInV5: String?,
- var name: String?) { // added in version 4, removed at 6
- companion object {
- val TABLE_NAME = "Entity3"
- }
- }
-
- @Entity(foreignKeys = arrayOf(ForeignKey(entity = Entity1::class,
- parentColumns = arrayOf("name"),
- childColumns = arrayOf("name"),
- deferred = true)))
- data class Entity4(@PrimaryKey var id: Int = 0, var name: String?) {
- companion object {
- val TABLE_NAME = "Entity4"
- }
- }
-
- @Dao
- internal interface MigrationDao {
- @Query("SELECT * from Entity1 ORDER BY id ASC")
- fun loadAllEntity1s(): List<Entity1>
-
- @Query("SELECT * from Entity2 ORDER BY id ASC")
- fun loadAllEntity2s(): List<Entity2>
-
- @Query("SELECT * from Entity2 ORDER BY id ASC")
- fun loadAllEntity2sAsPojo(): List<Entity2Pojo>
-
- @Insert
- fun insert(vararg entity2: Entity2)
- }
-
- internal class Entity2Pojo(id: Int, addedInV3: String?, name: String?)
- : Entity2(id, addedInV3, name)
-
- /**
- * not a real dao because database will change.
- */
- internal class Dao_V1(val mDb: SupportSQLiteDatabase) {
-
- fun insertIntoEntity1(id: Int, name: String) {
- val values = ContentValues()
- values.put("id", id)
- values.put("name", name)
- val insertionId = mDb.insert(Entity1.TABLE_NAME,
- SQLiteDatabase.CONFLICT_REPLACE, values)
- if (insertionId == -1L) {
- throw RuntimeException("test sanity failure")
- }
- }
- }
-
- /**
- * not a real dao because database will change.
- */
- internal class Dao_V2(val mDb: SupportSQLiteDatabase) {
-
- fun insertIntoEntity2(id: Int, name: String) {
- val values = ContentValues()
- values.put("id", id)
- values.put("name", name)
- val insertionId = mDb.insert(Entity2.TABLE_NAME,
- SQLiteDatabase.CONFLICT_REPLACE, values)
- if (insertionId == -1L) {
- throw RuntimeException("test sanity failure")
- }
- }
- }
-
- companion object {
- const val LATEST_VERSION = 7
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
deleted file mode 100644
index b684923..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * 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.kotlintestapp.migration
-
-import android.arch.persistence.db.SupportSQLiteDatabase
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory
-import android.arch.persistence.room.Room
-import android.arch.persistence.room.migration.Migration
-import android.arch.persistence.room.testing.MigrationTestHelper
-import android.arch.persistence.room.util.TableInfo
-import android.support.test.InstrumentationRegistry
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.containsString
-import org.hamcrest.CoreMatchers.instanceOf
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Rule
-import org.junit.Test
-import java.io.FileNotFoundException
-import java.io.IOException
-
-class MigrationKotlinTest {
-
- @get:Rule
- var helper: MigrationTestHelper = MigrationTestHelper(
- InstrumentationRegistry.getInstrumentation(),
- MigrationDbKotlin::class.java.canonicalName,
- FrameworkSQLiteOpenHelperFactory())
-
- companion object {
- val TEST_DB = "migration-test"
- }
-
- @Test
- @Throws(IOException::class)
- fun giveBadResource() {
- val helper = MigrationTestHelper(
- InstrumentationRegistry.getInstrumentation(),
- "foo", FrameworkSQLiteOpenHelperFactory())
- try {
- helper.createDatabase(TEST_DB, 1)
- throw AssertionError("must have failed with missing file exception")
- } catch (exception: FileNotFoundException) {
- assertThat<String>(exception.message, containsString("Cannot find"))
- }
- }
-
- @Test
- @Throws(IOException::class)
- fun startInCurrentVersion() {
- val db = helper.createDatabase(TEST_DB,
- MigrationDbKotlin.LATEST_VERSION)
- val dao = MigrationDbKotlin.Dao_V1(db)
- dao.insertIntoEntity1(2, "x")
- db.close()
- val migrationDb = getLatestDb()
- val items = migrationDb.dao().loadAllEntity1s()
- helper.closeWhenFinished(migrationDb)
- assertThat<Int>(items.size, `is`<Int>(1))
- }
-
- @Test
- @Throws(IOException::class)
- fun addTable() {
- var db = helper.createDatabase(TEST_DB, 1)
- val dao = MigrationDbKotlin.Dao_V1(db)
- dao.insertIntoEntity1(2, "foo")
- dao.insertIntoEntity1(3, "bar")
- db.close()
- db = helper.runMigrationsAndValidate(TEST_DB, 2, true,
- MIGRATION_1_2)
- MigrationDbKotlin.Dao_V2(db).insertIntoEntity2(3, "blah")
- db.close()
- val migrationDb = getLatestDb()
- val entity1s = migrationDb.dao().loadAllEntity1s()
-
- assertThat(entity1s.size, `is`(2))
- val entity2 = MigrationDbKotlin.Entity2(2, null, "bar")
- // assert no error happens
- migrationDb.dao().insert(entity2)
- val entity2s = migrationDb.dao().loadAllEntity2s()
- assertThat(entity2s.size, `is`(2))
- }
-
- private fun getLatestDb(): MigrationDbKotlin {
- val db = Room.databaseBuilder(
- InstrumentationRegistry.getInstrumentation().targetContext,
- MigrationDbKotlin::class.java, TEST_DB).addMigrations(*ALL_MIGRATIONS).build()
- // trigger open
- db.beginTransaction()
- db.endTransaction()
- helper.closeWhenFinished(db)
- return db
- }
-
- @Test
- @Throws(IOException::class)
- fun addTableFailure() {
- testFailure(1, 2)
- }
-
- @Test
- @Throws(IOException::class)
- fun addColumnFailure() {
- val db = helper.createDatabase(TEST_DB, 2)
- db.close()
- var caught: IllegalStateException? = null
- try {
- helper.runMigrationsAndValidate(TEST_DB, 3, true,
- EmptyMigration(2, 3))
- } catch (ex: IllegalStateException) {
- caught = ex
- }
-
- assertThat<IllegalStateException>(caught,
- instanceOf<IllegalStateException>(IllegalStateException::class.java))
- }
-
- @Test
- @Throws(IOException::class)
- fun addColumn() {
- val db = helper.createDatabase(TEST_DB, 2)
- val v2Dao = MigrationDbKotlin.Dao_V2(db)
- v2Dao.insertIntoEntity2(7, "blah")
- db.close()
- helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3)
- // trigger open.
- val migrationDb = getLatestDb()
- val entity2s = migrationDb.dao().loadAllEntity2s()
- assertThat(entity2s.size, `is`(1))
- assertThat<String>(entity2s[0].name, `is`("blah"))
- assertThat<String>(entity2s[0].addedInV3, `is`<Any>(nullValue()))
-
- val entity2Pojos = migrationDb.dao().loadAllEntity2sAsPojo()
- assertThat(entity2Pojos.size, `is`(1))
- assertThat<String>(entity2Pojos[0].name, `is`("blah"))
- assertThat<String>(entity2Pojos[0].addedInV3, `is`<Any>(nullValue()))
- }
-
- @Test
- @Throws(IOException::class)
- fun failedToRemoveColumn() {
- testFailure(4, 5)
- }
-
- @Test
- @Throws(IOException::class)
- fun removeColumn() {
- helper.createDatabase(TEST_DB, 4)
- val db = helper.runMigrationsAndValidate(TEST_DB,
- 5, true, MIGRATION_4_5)
- val info = TableInfo.read(db, MigrationDbKotlin.Entity3.TABLE_NAME)
- assertThat(info.columns.size, `is`(2))
- }
-
- @Test
- @Throws(IOException::class)
- fun dropTable() {
- helper.createDatabase(TEST_DB, 5)
- val db = helper.runMigrationsAndValidate(TEST_DB,
- 6, true, MIGRATION_5_6)
- val info = TableInfo.read(db, MigrationDbKotlin.Entity3.TABLE_NAME)
- assertThat(info.columns.size, `is`(0))
- }
-
- @Test
- @Throws(IOException::class)
- fun failedToDropTable() {
- testFailure(5, 6)
- }
-
- @Test
- @Throws(IOException::class)
- fun failedToDropTableDontVerify() {
- helper.createDatabase(TEST_DB, 5)
- val db = helper.runMigrationsAndValidate(TEST_DB,
- 6, false, EmptyMigration(5, 6))
- val info = TableInfo.read(db, MigrationDbKotlin.Entity3.TABLE_NAME)
- assertThat(info.columns.size, `is`(2))
- }
-
- @Test
- @Throws(IOException::class)
- fun failedForeignKey() {
- val db = helper.createDatabase(TEST_DB, 6)
- db.close()
- var throwable: Throwable? = null
- try {
- helper.runMigrationsAndValidate(TEST_DB,
- 7, false, object : Migration(6, 7) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE Entity4 (`id` INTEGER, `name` TEXT,"
- + " PRIMARY KEY(`id`))")
- }
- })
- } catch (t: Throwable) {
- throwable = t
- }
-
- assertThat<Throwable>(throwable, instanceOf<Throwable>(IllegalStateException::class.java))
-
- assertThat<String>(throwable!!.message, containsString("Migration failed"))
- }
-
- @Test
- @Throws(IOException::class)
- fun newTableWithForeignKey() {
- helper.createDatabase(TEST_DB, 6)
- val db = helper.runMigrationsAndValidate(TEST_DB,
- 7, false, MIGRATION_6_7)
- val info = TableInfo.read(db, MigrationDbKotlin.Entity4.TABLE_NAME)
- assertThat(info.foreignKeys.size, `is`(1))
- }
-
- @Throws(IOException::class)
- private fun testFailure(startVersion: Int, endVersion: Int) {
- val db = helper.createDatabase(TEST_DB, startVersion)
- db.close()
- var throwable: Throwable? = null
- try {
- helper.runMigrationsAndValidate(TEST_DB, endVersion, true,
- EmptyMigration(startVersion, endVersion))
- } catch (t: Throwable) {
- throwable = t
- }
-
- assertThat<Throwable>(throwable, instanceOf<Throwable>(IllegalStateException::class.java))
- assertThat<String>(throwable!!.message, containsString("Migration failed"))
- }
-
- internal val MIGRATION_1_2: Migration = object : Migration(1, 2) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` (`id` INTEGER NOT NULL,"
- + " `name` TEXT, PRIMARY KEY(`id`))")
- }
- }
-
- internal val MIGRATION_2_3: Migration = object : Migration(2, 3) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE " + MigrationDbKotlin.Entity2.TABLE_NAME
- + " ADD COLUMN addedInV3 TEXT")
- }
- }
-
- internal val MIGRATION_3_4: Migration = object : Migration(3, 4) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER NOT NULL,"
- + " `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))")
- }
- }
-
- internal val MIGRATION_4_5: Migration = object : Migration(4, 5) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER NOT NULL,"
- + " `name` TEXT, PRIMARY KEY(`id`))")
- database.execSQL("INSERT INTO Entity3_New(`id`, `name`) "
- + "SELECT `id`, `name` FROM Entity3")
- database.execSQL("DROP TABLE Entity3")
- database.execSQL("ALTER TABLE Entity3_New RENAME TO Entity3")
- }
- }
-
- internal val MIGRATION_5_6: Migration = object : Migration(5, 6) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("DROP TABLE " + MigrationDbKotlin.Entity3.TABLE_NAME)
- }
- }
-
- internal val MIGRATION_6_7: Migration = object : Migration(6, 7) {
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("CREATE TABLE IF NOT EXISTS "
- + MigrationDbKotlin.Entity4.TABLE_NAME
- + " (`id` INTEGER NOT NULL, `name` TEXT, 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 "
- + MigrationDbKotlin.Entity1.TABLE_NAME + " (`name`)")
- }
- }
-
- private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
- MIGRATION_5_6, MIGRATION_6_7)
-
- internal class EmptyMigration(startVersion: Int, endVersion: Int)
- : Migration(startVersion, endVersion) {
-
- override fun migrate(database: SupportSQLiteDatabase) {
- // do nothing
- }
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
deleted file mode 100644
index 5f355fd..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * 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.kotlintestapp.test
-
-import android.arch.core.executor.ArchTaskExecutor
-import android.arch.persistence.room.integration.kotlintestapp.vo.Author
-import android.arch.persistence.room.integration.kotlintestapp.vo.Book
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
-import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
-import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
-import android.database.sqlite.SQLiteConstraintException
-import android.support.test.filters.SmallTest
-import com.google.common.base.Optional
-import io.reactivex.Flowable
-import io.reactivex.schedulers.Schedulers
-import io.reactivex.subscribers.TestSubscriber
-import org.hamcrest.CoreMatchers
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.equalTo
-import org.hamcrest.CoreMatchers.instanceOf
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Assert.assertNotNull
-import org.junit.Test
-import java.util.Date
-
-@SmallTest
-class BooksDaoTest : TestDatabaseTest() {
-
- @Test
- fun bookById() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- assertThat(booksDao.getBook(TestUtil.BOOK_1.bookId), `is`<Book>(TestUtil.BOOK_1))
- }
-
- @Test
- fun bookByIdJavaOptional() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- assertThat(
- booksDao.getBookJavaOptional(TestUtil.BOOK_1.bookId),
- `is`<java.util.Optional<Book>>(java.util.Optional.of(TestUtil.BOOK_1)))
- }
-
- @Test
- fun bookByIdJavaOptionalEmpty() {
- assertThat(
- booksDao.getBookJavaOptional(TestUtil.BOOK_1.bookId),
- `is`<java.util.Optional<Book>>(java.util.Optional.empty()))
- }
-
- @Test
- fun bookByIdListenableFuture() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- assertThat(
- booksDao.getBookListenableFuture(TestUtil.BOOK_1.bookId).get(),
- `is`<Book>(TestUtil.BOOK_1))
- }
-
- @Test
- fun bookByIdOptional() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- assertThat(
- booksDao.getBookOptional(TestUtil.BOOK_1.bookId),
- `is`<Optional<Book>>(Optional.of(TestUtil.BOOK_1)))
- }
-
- @Test
- fun bookByIdOptionalListenableFuture() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- assertThat(
- booksDao.getBookOptionalListenableFuture(TestUtil.BOOK_1.bookId).get(),
- `is`<Optional<Book>>(Optional.of(TestUtil.BOOK_1)))
- }
-
- @Test
- fun bookByIdOptionalListenableFutureAbsent() {
- assertThat(
- booksDao.getBookOptionalListenableFuture(TestUtil.BOOK_1.bookId).get(),
- `is`<Optional<Book>>(Optional.absent()))
- }
-
- @Test
- fun bookByIdOptionalAbsent() {
- assertThat(
- booksDao.getBookOptional(TestUtil.BOOK_1.bookId),
- `is`<Optional<Book>>(Optional.absent()))
- }
-
- @Test
- fun bookByIdOptionalFlowable() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- val subscriber = TestSubscriber<Optional<Book>>()
- val flowable: Flowable<Optional<Book>> =
- booksDao.getBookOptionalFlowable(TestUtil.BOOK_1.bookId)
- flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
- .subscribeWith(subscriber)
-
- assertThat(subscriber.values().size, `is`(1))
- assertThat(subscriber.values()[0], `is`(Optional.of(TestUtil.BOOK_1)))
- }
-
- @Test
- fun bookByIdOptionalFlowableAbsent() {
- val subscriber = TestSubscriber<Optional<Book>>()
- val flowable: Flowable<Optional<Book>> =
- booksDao.getBookOptionalFlowable(TestUtil.BOOK_1.bookId)
- flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
- .subscribeWith(subscriber)
-
- assertThat(subscriber.values().size, `is`(1))
- assertThat(subscriber.values()[0], `is`(Optional.absent()))
- }
-
- @Test
- fun bookWithPublisher() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- val expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
- TestUtil.PUBLISHER)
- val expectedList = ArrayList<BookWithPublisher>()
- expectedList.add(expected)
-
- assertThat(database.booksDao().getBooksWithPublisher(),
- `is`<List<BookWithPublisher>>(expectedList))
- }
-
- @Test
- fun bookWithPublisherListenableFuture() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- val expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
- TestUtil.PUBLISHER)
- val expectedList = ArrayList<BookWithPublisher>()
- expectedList.add(expected)
-
- assertThat(database.booksDao().getBooksWithPublisherListenableFuture().get(),
- `is`<List<BookWithPublisher>>(expectedList))
- }
-
- @Test
- fun updateBookWithNullTitle() {
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- var throwable: Throwable? = null
- try {
- booksDao.updateBookTitle(TestUtil.BOOK_1.bookId, null)
- } catch (t: Throwable) {
- throwable = t
- }
- assertNotNull(throwable)
- assertThat<Throwable>(throwable, instanceOf<Throwable>(SQLiteConstraintException::class
- .java))
- }
-
- @Test
- fun publisherWithBooks() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
-
- val actualPublisherWithBooks = booksDao.getPublisherWithBooks(
- TestUtil.PUBLISHER.publisherId)
-
- assertThat(actualPublisherWithBooks.publisher, `is`<Publisher>(TestUtil.PUBLISHER))
- assertThat(actualPublisherWithBooks.books?.size, `is`(2))
- assertThat(actualPublisherWithBooks.books?.get(0), `is`<Book>(TestUtil.BOOK_1))
- assertThat(actualPublisherWithBooks.books?.get(1), `is`<Book>(TestUtil.BOOK_2))
- }
-
- @Test // b/68077506
- fun publisherWithBookSales() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
- val actualPublisherWithBooks = booksDao.getPublisherWithBookSales(
- TestUtil.PUBLISHER.publisherId)
-
- assertThat(actualPublisherWithBooks.publisher, `is`<Publisher>(TestUtil.PUBLISHER))
- assertThat(actualPublisherWithBooks.sales, `is`(listOf(TestUtil.BOOK_1.salesCnt,
- TestUtil.BOOK_2.salesCnt)))
- }
-
- @Test
- fun insertAuthorWithAllFields() {
- val author = Author("id", "name", Date(), ArrayList())
- database.booksDao().addAuthors(author)
-
- val authorDb = database.booksDao().getAuthor(author.authorId)
-
- assertThat(authorDb, CoreMatchers.`is`<Author>(author))
- }
-
- @Test
- fun insertInInheritedDao() {
- database.derivedDao().insert(TestUtil.AUTHOR_1)
-
- val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
-
- assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
- }
-
- @Test
- fun findBooksInMultiLineQuery() {
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
- booksDao.addBooks(TestUtil.BOOK_2)
-
- val books = database.booksDao().getBooksMultiLineQuery(arrayListOf(
- TestUtil.BOOK_1.bookId,
- TestUtil.BOOK_2.bookId))
- assertThat(books, `is`(listOf(TestUtil.BOOK_2, TestUtil.BOOK_1)))
- }
-
- @Test
- fun findBooksByLanguage() {
- booksDao.addPublishers(TestUtil.PUBLISHER)
- val book1 = TestUtil.BOOK_1.copy(languages = setOf(Lang.TR))
- val book2 = TestUtil.BOOK_2.copy(languages = setOf(Lang.ES, Lang.TR))
- val book3 = TestUtil.BOOK_3.copy(languages = setOf(Lang.EN))
- booksDao.addBooks(book1, book2, book3)
-
- assertThat(booksDao.findByLanguages(setOf(Lang.EN, Lang.TR)),
- `is`(listOf(book1, book2, book3)))
-
- assertThat(booksDao.findByLanguages(setOf(Lang.TR)),
- `is`(listOf(book1, book2)))
-
- assertThat(booksDao.findByLanguages(setOf(Lang.ES)),
- `is`(listOf(book2)))
-
- assertThat(booksDao.findByLanguages(setOf(Lang.EN)),
- `is`(listOf(book3)))
- }
-
- @Test
- fun insertVarargInInheritedDao() {
- database.derivedDao().insertAllArg(TestUtil.AUTHOR_1, TestUtil.AUTHOR_2)
-
- val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
-
- assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
- }
-
- @Test
- fun insertListInInheritedDao() {
- database.derivedDao().insertAll(listOf(TestUtil.AUTHOR_1))
-
- val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
-
- assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
- }
-
- @Test
- fun deleteAndAddPublisher() {
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.getPublishers().run {
- assertThat(this.size, `is`(1))
- assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER)))
- }
- booksDao.deleteAndAddPublisher(TestUtil.PUBLISHER, TestUtil.PUBLISHER2)
- booksDao.getPublishers().run {
- assertThat(this.size, `is`(1))
- assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER2)))
- }
- }
-
- @Test
- fun deleteAndAddPublisher_failure() {
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.getPublishers().run {
- assertThat(this.size, `is`(1))
- assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER)))
- }
- var throwable: Throwable? = null
- try {
- booksDao.deleteAndAddPublisher(TestUtil.PUBLISHER, TestUtil.PUBLISHER2, true)
- } catch (e: RuntimeException) {
- throwable = e
- }
- assertThat(throwable, `is`(notNullValue()))
- booksDao.getPublishers().run {
- assertThat(this.size, `is`(1))
- assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER)))
- }
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/DependencyDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/DependencyDaoTest.kt
deleted file mode 100644
index fd6de54..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/DependencyDaoTest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2018 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.kotlintestapp.test
-
-import android.arch.persistence.room.integration.kotlintestapp.dao.DependencyDao
-import android.arch.persistence.room.integration.kotlintestapp.vo.DataClassFromDependency
-import android.arch.persistence.room.integration.kotlintestapp.vo.EmbeddedFromDependency
-import android.arch.persistence.room.integration.kotlintestapp.vo.PojoFromDependency
-import android.os.Build
-import android.support.test.filters.SdkSuppress
-import android.support.test.runner.AndroidJUnit4
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class DependencyDaoTest : TestDatabaseTest() {
- lateinit var dao: DependencyDao
- @Before
- fun init() {
- dao = database.dependencyDao()
- }
-
- @Test
- fun insertAndGet() {
- val data = insertSample(3)
- assertThat(dao.selectAll(), `is`(listOf(data)))
- }
-
- @Test
- fun insertAndGetByQuery() {
- val data = insertSample(3)
- assertThat(dao.findById(3), `is`(data))
- assertThat(dao.findById(5), `is`(nullValue()))
- }
-
- @Test
- fun insertAndGetByQuery_embedded() {
- val data = insertSample(3)
- assertThat(dao.findEmbedded(3), `is`(EmbeddedFromDependency(data)))
- assertThat(dao.findEmbedded(5), `is`(nullValue()))
- }
-
- @Test
- fun insertAndGetByQuery_pojo() {
- val data = insertSample(3)
- assertThat(dao.findPojo(3), `is`(PojoFromDependency(
- id = data.id,
- name = data.name)))
- assertThat(dao.findPojo(5), `is`(nullValue()))
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- @Test
- fun getRelation() {
- val foo1 = DataClassFromDependency(
- id = 3,
- name = "foo"
- )
- val foo2 = DataClassFromDependency(
- id = 4,
- name = "foo"
- )
- val bar = DataClassFromDependency(
- id = 5,
- name = "bar"
- )
- dao.insert(foo1, foo2, bar)
- val fooList = dao.relation("foo")
- assertThat(fooList.sharedName, `is`("foo"))
- assertThat(fooList, `is`(notNullValue()))
- assertThat(fooList.dataItems, `is`(listOf(foo1, foo2)))
-
- val barList = dao.relation("bar")
- assertThat(barList.sharedName, `is`("bar"))
- assertThat(barList, `is`(notNullValue()))
- assertThat(barList.dataItems, `is`(listOf(bar)))
-
- val bazList = dao.relation("baz")
- assertThat(bazList.sharedName, `is`("baz"))
- assertThat(bazList, `is`(notNullValue()))
- assertThat(bazList.dataItems, `is`(emptyList()))
- }
-
- private fun insertSample(id: Int): DataClassFromDependency {
- val data = DataClassFromDependency(
- id = id,
- name = "foo"
- )
- dao.insert(data)
- return data
- }
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/ItemWithNullableConstructor.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/ItemWithNullableConstructor.kt
deleted file mode 100644
index 091beae..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/ItemWithNullableConstructor.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2018 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.kotlintestapp.test
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Database
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.Room
-import android.arch.persistence.room.RoomDatabase
-import android.arch.persistence.room.RoomWarnings
-import android.support.test.InstrumentationRegistry
-import android.support.test.runner.AndroidJUnit4
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ItemWithNullableConstructor {
- lateinit var db: Db
- @Before
- fun initDb() {
- db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
- Db::class.java).build()
- }
-
- @After
- fun closeDb() {
- db.close()
- }
-
- @Test
- fun insertWithNull() {
- db.dao.insert(TestItem(null, null))
- assertThat(db.dao.get(), `is`(TestItem(1, null)))
- }
-
- @Entity
- data class TestItem(
- @PrimaryKey(autoGenerate = true)
- val id: Long? = null,
- val nullable: Boolean?
- )
-
- @Dao
- interface TestDao {
- @Insert
- fun insert(testItem: TestItem)
-
- @Query("SELECT * FROM TestItem LIMIT 1")
- fun get(): TestItem?
- }
-
- @Database(
- version = 1,
- entities = [TestItem::class],
- exportSchema = false
- )
- @SuppressWarnings(RoomWarnings.MISSING_SCHEMA_LOCATION)
- abstract class Db : RoomDatabase() {
- abstract val dao: TestDao
- }
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/LiveDataQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/LiveDataQueryTest.kt
deleted file mode 100644
index 70bc54c..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/LiveDataQueryTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.kotlintestapp.test
-
-import android.arch.persistence.room.integration.kotlintestapp.vo.Book
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
-import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
-import android.support.test.filters.SmallTest
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Test
-
-@SmallTest
-class LiveDataQueryTest : TestDatabaseTest() {
-
- @Test
- fun observeBooksById() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- val book = LiveDataTestUtil.getValue(booksDao.getBookLiveData(TestUtil.BOOK_1.bookId))
-
- assertThat(book, `is`<Book>(TestUtil.BOOK_1))
- }
-
- @Test
- fun observeBooksWithPublisher() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- var expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
- TestUtil.PUBLISHER)
- var expectedList = ArrayList<BookWithPublisher>()
- expectedList.add(expected)
-
- val actual = LiveDataTestUtil.getValue(booksDao.getBooksWithPublisherLiveData())
- assertThat(actual, `is`<List<BookWithPublisher>>(expectedList))
- }
-
- @Test
- fun publisherWithBooks() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
-
- var actualPublisherWithBooks = LiveDataTestUtil.getValue(
- booksDao.getPublisherWithBooksLiveData(TestUtil.PUBLISHER.publisherId))
-
- assertThat(actualPublisherWithBooks.publisher, `is`<Publisher>(TestUtil.PUBLISHER))
- assertThat(actualPublisherWithBooks.books?.size, `is`(2))
- assertThat(actualPublisherWithBooks.books?.get(0), `is`<Book>(TestUtil.BOOK_1))
- assertThat(actualPublisherWithBooks.books?.get(1), `is`<Book>(TestUtil.BOOK_2))
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/LiveDataTestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
deleted file mode 100644
index 5533f11..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.kotlintestapp.test
-
-import android.arch.lifecycle.LiveData
-import android.arch.lifecycle.Observer
-
-object LiveDataTestUtil {
-
- @Throws(InterruptedException::class)
- fun <T> getValue(liveData: LiveData<T>): T {
- val data = arrayOfNulls<Any>(1)
- val observer = object : Observer<T> {
- override fun onChanged(o: T?) {
- data[0] = o
- liveData.removeObserver(this)
- }
- }
- liveData.observeForever(observer)
-
- return data[0] as T
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/RxJava2QueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
deleted file mode 100644
index 094eea8..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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.kotlintestapp.test
-
-import android.arch.persistence.room.EmptyResultSetException
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
-import android.support.test.filters.SmallTest
-import org.junit.Test
-
-@SmallTest
-class RxJava2QueryTest : TestDatabaseTest() {
-
- @Test
- fun observeBooksById() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- booksDao.getBookFlowable(TestUtil.BOOK_1.bookId)
- .test()
- .assertValue { book -> book == TestUtil.BOOK_1 }
- }
-
- @Test
- fun observeBooksByIdSingle() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- booksDao.getBookSingle(TestUtil.BOOK_1.bookId)
- .test()
- .assertComplete()
- .assertValue { book -> book == TestUtil.BOOK_1 }
- }
-
- @Test
- fun observeBooksByIdSingle_noBook() {
- booksDao.getBookSingle("x")
- .test()
- .assertError(EmptyResultSetException::class.java)
- }
-
- @Test
- fun observeBooksByIdMaybe() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- booksDao.getBookMaybe(TestUtil.BOOK_1.bookId)
- .test()
- .assertComplete()
- .assertValue { book -> book == TestUtil.BOOK_1 }
- }
-
- @Test
- fun observeBooksByIdMaybe_noBook() {
- booksDao.getBookMaybe("x")
- .test()
- .assertComplete()
- .assertNoErrors()
- .assertNoValues()
- }
-
- @Test
- fun observeBooksWithPublisher() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1)
-
- var expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
- TestUtil.PUBLISHER)
- var expectedList = ArrayList<BookWithPublisher>()
- expectedList.add(expected)
-
- booksDao.getBooksWithPublisherFlowable()
- .test()
- .assertValue(expectedList)
- }
-
- @Test
- fun publisherWithBooks() {
- booksDao.addAuthors(TestUtil.AUTHOR_1)
- booksDao.addPublishers(TestUtil.PUBLISHER)
- booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
-
- booksDao.getPublisherWithBooksFlowable(TestUtil.PUBLISHER.publisherId)
- .test()
- .assertValue {
- it.publisher == TestUtil.PUBLISHER
- && it.books?.size == 2
- && it.books?.get(0) == TestUtil.BOOK_1
- && it.books?.get(1) == TestUtil.BOOK_2
- }
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestDatabaseTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestDatabaseTest.kt
deleted file mode 100644
index 520c4d3..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestDatabaseTest.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.kotlintestapp.test
-
-import android.arch.core.executor.testing.InstantTaskExecutorRule
-import android.arch.persistence.room.Room
-import android.arch.persistence.room.integration.kotlintestapp.dao.BooksDao
-import android.arch.persistence.room.integration.kotlintestapp.TestDatabase
-import android.support.test.InstrumentationRegistry
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-
-abstract class TestDatabaseTest {
-
- @get:Rule
- var instantTaskExecutorRule = InstantTaskExecutorRule()
-
- protected lateinit var database: TestDatabase
- protected lateinit var booksDao: BooksDao
-
- @Before
- @Throws(Exception::class)
- fun setUp() {
- database = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
- TestDatabase::class.java)
- // allowing main thread queries, just for testing
- .allowMainThreadQueries()
- .build()
-
- booksDao = database.booksDao()
- }
-
- @After
- @Throws(Exception::class)
- fun tearDown() {
- database.close()
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
deleted file mode 100644
index 8c02a64..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.kotlintestapp.test
-
-import android.arch.persistence.room.integration.kotlintestapp.vo.Author
-import android.arch.persistence.room.integration.kotlintestapp.vo.Book
-import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
-import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
-import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
-
-class TestUtil {
-
- companion object {
-
- val PUBLISHER = Publisher("ph1", "publisher 1")
- val PUBLISHER2 = Publisher("ph2", "publisher 2")
-
- val AUTHOR_1 = Author("a1", "author 1")
- val AUTHOR_2 = Author("a2", "author 2")
-
- val BOOK_1 = Book("b1", "book title 1", "ph1",
- setOf(Lang.EN), 3)
- val BOOK_2 = Book("b2", "book title 2", "ph1",
- setOf(Lang.TR), 5)
- val BOOK_3 = Book("b3", "book title 2", "ph1",
- setOf(Lang.ES), 7)
-
- val BOOK_AUTHOR_1_1 = BookAuthor(BOOK_1.bookId, AUTHOR_1.authorId)
- val BOOK_AUTHOR_1_2 = BookAuthor(BOOK_1.bookId, AUTHOR_2.authorId)
- val BOOK_AUTHOR_2_2 = BookAuthor(BOOK_2.bookId, AUTHOR_2.authorId)
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Author.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Author.kt
deleted file mode 100644
index 067fcb3..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Author.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.TypeConverters
-import java.util.Date
-
-@Entity
-@TypeConverters(DateConverter::class, StringToIntListConverters::class)
-data class Author(
- @PrimaryKey val authorId: String,
- val name: String,
- val dateOfBirth: Date? = null,
- val aList: List<Integer>? = null)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
deleted file mode 100644
index 019f07f..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.ForeignKey
-import android.arch.persistence.room.PrimaryKey
-import android.arch.persistence.room.TypeConverters
-
-@Entity(foreignKeys = arrayOf(
- ForeignKey(entity = Publisher::class,
- parentColumns = arrayOf("publisherId"),
- childColumns = arrayOf("bookPublisherId"),
- deferred = true)))
-data class Book(
- @PrimaryKey val bookId: String,
- val title: String,
- val bookPublisherId: String,
- @field:TypeConverters(Lang::class)
- val languages: Set<Lang>,
- val salesCnt: Int)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/BookAuthor.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/BookAuthor.kt
deleted file mode 100644
index 5a21f40..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/BookAuthor.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.ForeignKey
-
-@Entity(foreignKeys = arrayOf(
- ForeignKey(entity = Book::class,
- parentColumns = arrayOf("bookId"),
- childColumns = arrayOf("bookId"),
- onUpdate = ForeignKey.CASCADE,
- onDelete = ForeignKey.CASCADE,
- deferred = true),
- ForeignKey(entity = Author::class,
- parentColumns = arrayOf("authorId"),
- childColumns = arrayOf("authorId"),
- onUpdate = ForeignKey.CASCADE,
- onDelete = ForeignKey.CASCADE,
- deferred = true)),
- primaryKeys = arrayOf("bookId", "authorId"))
-data class BookAuthor(val bookId: String, val authorId: String)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/BookWithPublisher.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/BookWithPublisher.kt
deleted file mode 100644
index 418a0e2..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/BookWithPublisher.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Embedded
-
-data class BookWithPublisher(val bookId: String, val title: String,
- @Embedded val publisher: Publisher)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/DateConverter.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/DateConverter.kt
deleted file mode 100755
index 62a3c21..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/DateConverter.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 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.kotlintestapp.vo
-
-import android.arch.persistence.room.TypeConverter
-
-import java.util.Date
-
-class DateConverter {
- @TypeConverter
- fun toDate(timestamp: Long?): Date? {
- return if (timestamp == null) null else Date(timestamp)
- }
-
- @TypeConverter
- fun toTimestamp(date: Date?): Long? {
- return date?.time
- }
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt
deleted file mode 100644
index 1d5fb5c..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 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.kotlintestapp.vo
-
-import android.arch.persistence.room.TypeConverter
-
-/**
- * An enum class which gets saved as a bit set in the database.
- */
-enum class Lang {
- TR,
- EN,
- ES;
-
- companion object {
- @JvmStatic
- @TypeConverter
- fun toInt(langs: Set<Lang>): Int {
- return langs.fold(0) { left, lang ->
- left.or(1 shl lang.ordinal)
- }
- }
-
- @JvmStatic
- @TypeConverter
- fun toSet(value: Int): Set<Lang> {
- return Lang.values().filter {
- (1 shl it.ordinal).and(value) != 0
- }.toSet()
- }
- }
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/NoArgClass.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/NoArgClass.kt
deleted file mode 100644
index e1e1a7d..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/NoArgClass.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-
-/**
- * just here to ensure that we handle no-arg constructors fine from kotlin.
- */
-@Entity
-data class NoArgClass(@PrimaryKey var id: Long = 0, var class_name: String = "")
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Publisher.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Publisher.kt
deleted file mode 100644
index ea1550a..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Publisher.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-
-@Entity
-data class Publisher(@PrimaryKey val publisherId: String, val name: String)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/PublisherWithBookSales.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/PublisherWithBookSales.kt
deleted file mode 100644
index 0087331..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/PublisherWithBookSales.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Embedded
-import android.arch.persistence.room.Relation
-
-data class PublisherWithBookSales @JvmOverloads constructor(
- @Embedded
- val publisher: Publisher,
- @Relation(parentColumn = "publisherId", // publisher.publisherId
- entityColumn = "bookPublisherId", // book.bookPublisherId
- entity = Book::class,
- projection = ["salesCnt"])
- var sales: List<Int>? = emptyList())
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/PublisherWithBooks.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/PublisherWithBooks.kt
deleted file mode 100644
index fe482b1..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/PublisherWithBooks.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Embedded
-import android.arch.persistence.room.Relation
-
-class PublisherWithBooks {
- @Embedded var publisher: Publisher? = null
- @Relation(parentColumn = "publisherId", // publisher.publisherId
- entityColumn = "bookPublisherId", // book.bookPublisherId
- entity = Book::class)
- var books: List<Book>? = null
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/StringToIntListConverters.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/StringToIntListConverters.kt
deleted file mode 100644
index ec8fd4a..0000000
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/StringToIntListConverters.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package android.arch.persistence.room.integration.kotlintestapp.vo
-
-import android.arch.persistence.room.TypeConverter
-import android.arch.persistence.room.util.StringUtil
-
-object StringToIntListConverters {
- @TypeConverter
- // Specifying that a static method should be generated. Otherwise, the compiler looks for the
- // constructor of the class, and a object has a private constructor.
- @JvmStatic
- fun stringToIntList(data: String?): List<Int>? =
- if (data == null) null else StringUtil.splitToIntList(data)
-
- @TypeConverter
- @JvmStatic
- fun intListToString(ints: List<Int>?): String? =
- if (ints == null) null else StringUtil.joinIntoString(ints)
-}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt
new file mode 100644
index 0000000..5e4e0e7
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.integration.kotlintestapp.dao.BooksDao
+import androidx.room.integration.kotlintestapp.dao.DependencyDao
+import androidx.room.integration.kotlintestapp.dao.DerivedDao
+import androidx.room.integration.kotlintestapp.vo.Author
+import androidx.room.integration.kotlintestapp.vo.Book
+import androidx.room.integration.kotlintestapp.vo.BookAuthor
+import androidx.room.integration.kotlintestapp.vo.DataClassFromDependency
+import androidx.room.integration.kotlintestapp.vo.NoArgClass
+import androidx.room.integration.kotlintestapp.vo.Publisher
+
+@Database(entities = [Book::class, Author::class, Publisher::class, BookAuthor::class,
+ NoArgClass::class, DataClassFromDependency::class], version = 1)
+abstract class TestDatabase : RoomDatabase() {
+
+ abstract fun booksDao(): BooksDao
+
+ abstract fun derivedDao(): DerivedDao
+
+ abstract fun dependencyDao(): DependencyDao
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BaseDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BaseDao.kt
new file mode 100644
index 0000000..a9aac29
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BaseDao.kt
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.dao
+
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Update
+
+interface BaseDao<T> {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(t: T)
+
+ @Insert
+ fun insertAll(t: List<T>)
+
+ @Insert
+ fun insertAllArg(vararg t: T)
+
+ @Update
+ fun update(t: T)
+
+ @Delete
+ fun delete(t: T)
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
new file mode 100644
index 0000000..1f8e992
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -0,0 +1,143 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.TypeConverters
+import androidx.room.integration.kotlintestapp.vo.Author
+import androidx.room.integration.kotlintestapp.vo.Book
+import androidx.room.integration.kotlintestapp.vo.BookAuthor
+import androidx.room.integration.kotlintestapp.vo.BookWithPublisher
+import androidx.room.integration.kotlintestapp.vo.Lang
+import androidx.room.integration.kotlintestapp.vo.Publisher
+import androidx.room.integration.kotlintestapp.vo.PublisherWithBookSales
+import androidx.room.integration.kotlintestapp.vo.PublisherWithBooks
+import com.google.common.base.Optional
+import com.google.common.util.concurrent.ListenableFuture
+import io.reactivex.Flowable
+import io.reactivex.Maybe
+import io.reactivex.Single
+
+@Dao
+interface BooksDao {
+
+ @Insert
+ fun addPublishers(vararg publishers: Publisher)
+
+ @Delete
+ fun deletePublishers(vararg publishers: Publisher)
+
+ @Insert
+ fun addAuthors(vararg authors: Author)
+
+ @Query("SELECT * FROM author WHERE authorId = :authorId")
+ fun getAuthor(authorId: String): Author
+
+ @Insert
+ fun addBooks(vararg books: Book)
+
+ @Insert
+ fun addBookAuthors(vararg bookAuthors: BookAuthor)
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBook(bookId: String): Book
+
+ @Query("""SELECT * FROM book WHERE
+ bookId IN(:bookIds)
+ order by bookId DESC""")
+ fun getBooksMultiLineQuery(bookIds: List<String>): List<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookLiveData(bookId: String): LiveData<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookFlowable(bookId: String): Flowable<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookJavaOptional(bookId: String): java.util.Optional<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookListenableFuture(bookId: String): ListenableFuture<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookOptional(bookId: String): Optional<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookOptionalFlowable(bookId: String): Flowable<Optional<Book>>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookOptionalListenableFuture(bookId: String): ListenableFuture<Optional<Book>>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookSingle(bookId: String): Single<Book>
+
+ @Query("SELECT * FROM book WHERE bookId = :bookId")
+ fun getBookMaybe(bookId: String): Maybe<Book>
+
+ @Query("SELECT * FROM book INNER JOIN publisher " +
+ "ON book.bookPublisherId = publisher.publisherId ")
+ fun getBooksWithPublisher(): List<BookWithPublisher>
+
+ @Query("SELECT * FROM book INNER JOIN publisher " +
+ "ON book.bookPublisherId = publisher.publisherId ")
+ fun getBooksWithPublisherLiveData(): LiveData<List<BookWithPublisher>>
+
+ @Query("SELECT * FROM book INNER JOIN publisher " +
+ "ON book.bookPublisherId = publisher.publisherId ")
+ fun getBooksWithPublisherFlowable(): Flowable<List<BookWithPublisher>>
+
+ @Query("SELECT * FROM book INNER JOIN publisher " +
+ "ON book.bookPublisherId = publisher.publisherId ")
+ fun getBooksWithPublisherListenableFuture(): ListenableFuture<List<BookWithPublisher>>
+
+ @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
+ fun getPublisherWithBooks(publisherId: String): PublisherWithBooks
+
+ @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
+ fun getPublisherWithBookSales(publisherId: String): PublisherWithBookSales
+
+ @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
+ fun getPublisherWithBooksLiveData(publisherId: String): LiveData<PublisherWithBooks>
+
+ @Query("SELECT * FROM publisher WHERE publisherId = :publisherId")
+ fun getPublisherWithBooksFlowable(publisherId: String): Flowable<PublisherWithBooks>
+
+ @Query("UPDATE book SET title = :title WHERE bookId = :bookId")
+ fun updateBookTitle(bookId: String, title: String?)
+
+ @Query("SELECT * FROM book WHERE languages & :langs != 0 ORDER BY bookId ASC")
+ @TypeConverters(Lang::class)
+ fun findByLanguages(langs: Set<Lang>): List<Book>
+
+ @Transaction
+ fun deleteAndAddPublisher(oldPublisher: Publisher, newPublisher: Publisher,
+ fail: Boolean = false) {
+ deletePublishers(oldPublisher)
+ if (fail) {
+ throw RuntimeException()
+ }
+ addPublishers(newPublisher)
+ }
+
+ @Query("SELECT * FROM Publisher")
+ fun getPublishers(): List<Publisher>
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
new file mode 100644
index 0000000..851263c
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/DerivedDao.kt
@@ -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 androidx.room.integration.kotlintestapp.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.integration.kotlintestapp.vo.Author
+
+@Dao
+interface DerivedDao : BaseDao<Author> {
+
+ @Query("SELECT * FROM author WHERE authorId = :authorId")
+ fun getAuthor(authorId: String): Author
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationDbKotlin.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationDbKotlin.kt
new file mode 100644
index 0000000..b65acd0
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationDbKotlin.kt
@@ -0,0 +1,128 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.migration
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Ignore
+import androidx.room.Index
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+@Database(version = MigrationDbKotlin.LATEST_VERSION,
+ entities = arrayOf(MigrationDbKotlin.Entity1::class, MigrationDbKotlin.Entity2::class,
+ MigrationDbKotlin.Entity4::class))
+abstract class MigrationDbKotlin : RoomDatabase() {
+
+ internal abstract fun dao(): MigrationDao
+
+ @Entity(indices = arrayOf(Index(value = "name", unique = true)))
+ data class Entity1(@PrimaryKey var id: Int = 0, var name: String?) {
+
+ companion object {
+ val TABLE_NAME = "Entity1"
+ }
+ }
+
+ @Entity
+ open class Entity2(@PrimaryKey var id: Int = 0, var addedInV3: String?, var name: String?) {
+ companion object {
+ val TABLE_NAME = "Entity2"
+ }
+ }
+
+ @Entity
+ data class Entity3(@PrimaryKey var id: Int = 0, @Ignore var removedInV5: String?,
+ var name: String?) { // added in version 4, removed at 6
+ companion object {
+ val TABLE_NAME = "Entity3"
+ }
+ }
+
+ @Entity(foreignKeys = arrayOf(ForeignKey(entity = Entity1::class,
+ parentColumns = arrayOf("name"),
+ childColumns = arrayOf("name"),
+ deferred = true)))
+ data class Entity4(@PrimaryKey var id: Int = 0, var name: String?) {
+ companion object {
+ val TABLE_NAME = "Entity4"
+ }
+ }
+
+ @Dao
+ internal interface MigrationDao {
+ @Query("SELECT * from Entity1 ORDER BY id ASC")
+ fun loadAllEntity1s(): List<Entity1>
+
+ @Query("SELECT * from Entity2 ORDER BY id ASC")
+ fun loadAllEntity2s(): List<Entity2>
+
+ @Query("SELECT * from Entity2 ORDER BY id ASC")
+ fun loadAllEntity2sAsPojo(): List<Entity2Pojo>
+
+ @Insert
+ fun insert(vararg entity2: Entity2)
+ }
+
+ internal class Entity2Pojo(id: Int, addedInV3: String?, name: String?)
+ : Entity2(id, addedInV3, name)
+
+ /**
+ * not a real dao because database will change.
+ */
+ internal class Dao_V1(val mDb: SupportSQLiteDatabase) {
+
+ fun insertIntoEntity1(id: Int, name: String) {
+ val values = ContentValues()
+ values.put("id", id)
+ values.put("name", name)
+ val insertionId = mDb.insert(Entity1.TABLE_NAME,
+ SQLiteDatabase.CONFLICT_REPLACE, values)
+ if (insertionId == -1L) {
+ throw RuntimeException("test sanity failure")
+ }
+ }
+ }
+
+ /**
+ * not a real dao because database will change.
+ */
+ internal class Dao_V2(val mDb: SupportSQLiteDatabase) {
+
+ fun insertIntoEntity2(id: Int, name: String) {
+ val values = ContentValues()
+ values.put("id", id)
+ values.put("name", name)
+ val insertionId = mDb.insert(Entity2.TABLE_NAME,
+ SQLiteDatabase.CONFLICT_REPLACE, values)
+ if (insertionId == -1L) {
+ throw RuntimeException("test sanity failure")
+ }
+ }
+ }
+
+ companion object {
+ const val LATEST_VERSION = 7
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
new file mode 100644
index 0000000..7185984
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -0,0 +1,305 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.migration
+
+import android.support.test.InstrumentationRegistry
+import androidx.room.Room
+import androidx.room.migration.Migration
+import androidx.room.testing.MigrationTestHelper
+import androidx.room.util.TableInfo
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Rule
+import org.junit.Test
+import java.io.FileNotFoundException
+import java.io.IOException
+
+class MigrationKotlinTest {
+
+ @get:Rule
+ var helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ MigrationDbKotlin::class.java.canonicalName,
+ FrameworkSQLiteOpenHelperFactory())
+
+ companion object {
+ val TEST_DB = "migration-test"
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun giveBadResource() {
+ val helper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ "foo", FrameworkSQLiteOpenHelperFactory())
+ try {
+ helper.createDatabase(TEST_DB, 1)
+ throw AssertionError("must have failed with missing file exception")
+ } catch (exception: FileNotFoundException) {
+ assertThat<String>(exception.message, containsString("Cannot find"))
+ }
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun startInCurrentVersion() {
+ val db = helper.createDatabase(TEST_DB,
+ MigrationDbKotlin.LATEST_VERSION)
+ val dao = MigrationDbKotlin.Dao_V1(db)
+ dao.insertIntoEntity1(2, "x")
+ db.close()
+ val migrationDb = getLatestDb()
+ val items = migrationDb.dao().loadAllEntity1s()
+ helper.closeWhenFinished(migrationDb)
+ assertThat<Int>(items.size, `is`<Int>(1))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun addTable() {
+ var db = helper.createDatabase(TEST_DB, 1)
+ val dao = MigrationDbKotlin.Dao_V1(db)
+ dao.insertIntoEntity1(2, "foo")
+ dao.insertIntoEntity1(3, "bar")
+ db.close()
+ db = helper.runMigrationsAndValidate(TEST_DB, 2, true,
+ MIGRATION_1_2)
+ MigrationDbKotlin.Dao_V2(db).insertIntoEntity2(3, "blah")
+ db.close()
+ val migrationDb = getLatestDb()
+ val entity1s = migrationDb.dao().loadAllEntity1s()
+
+ assertThat(entity1s.size, `is`(2))
+ val entity2 = MigrationDbKotlin.Entity2(2, null, "bar")
+ // assert no error happens
+ migrationDb.dao().insert(entity2)
+ val entity2s = migrationDb.dao().loadAllEntity2s()
+ assertThat(entity2s.size, `is`(2))
+ }
+
+ private fun getLatestDb(): MigrationDbKotlin {
+ val db = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ MigrationDbKotlin::class.java, TEST_DB).addMigrations(*ALL_MIGRATIONS).build()
+ // trigger open
+ db.beginTransaction()
+ db.endTransaction()
+ helper.closeWhenFinished(db)
+ return db
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun addTableFailure() {
+ testFailure(1, 2)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun addColumnFailure() {
+ val db = helper.createDatabase(TEST_DB, 2)
+ db.close()
+ var caught: IllegalStateException? = null
+ try {
+ helper.runMigrationsAndValidate(TEST_DB, 3, true,
+ EmptyMigration(2, 3))
+ } catch (ex: IllegalStateException) {
+ caught = ex
+ }
+
+ assertThat<IllegalStateException>(caught,
+ instanceOf<IllegalStateException>(IllegalStateException::class.java))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun addColumn() {
+ val db = helper.createDatabase(TEST_DB, 2)
+ val v2Dao = MigrationDbKotlin.Dao_V2(db)
+ v2Dao.insertIntoEntity2(7, "blah")
+ db.close()
+ helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3)
+ // trigger open.
+ val migrationDb = getLatestDb()
+ val entity2s = migrationDb.dao().loadAllEntity2s()
+ assertThat(entity2s.size, `is`(1))
+ assertThat<String>(entity2s[0].name, `is`("blah"))
+ assertThat<String>(entity2s[0].addedInV3, `is`<Any>(nullValue()))
+
+ val entity2Pojos = migrationDb.dao().loadAllEntity2sAsPojo()
+ assertThat(entity2Pojos.size, `is`(1))
+ assertThat<String>(entity2Pojos[0].name, `is`("blah"))
+ assertThat<String>(entity2Pojos[0].addedInV3, `is`<Any>(nullValue()))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun failedToRemoveColumn() {
+ testFailure(4, 5)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun removeColumn() {
+ helper.createDatabase(TEST_DB, 4)
+ val db = helper.runMigrationsAndValidate(TEST_DB,
+ 5, true, MIGRATION_4_5)
+ val info = TableInfo.read(db, MigrationDbKotlin.Entity3.TABLE_NAME)
+ assertThat(info.columns.size, `is`(2))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun dropTable() {
+ helper.createDatabase(TEST_DB, 5)
+ val db = helper.runMigrationsAndValidate(TEST_DB,
+ 6, true, MIGRATION_5_6)
+ val info = TableInfo.read(db, MigrationDbKotlin.Entity3.TABLE_NAME)
+ assertThat(info.columns.size, `is`(0))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun failedToDropTable() {
+ testFailure(5, 6)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun failedToDropTableDontVerify() {
+ helper.createDatabase(TEST_DB, 5)
+ val db = helper.runMigrationsAndValidate(TEST_DB,
+ 6, false, EmptyMigration(5, 6))
+ val info = TableInfo.read(db, MigrationDbKotlin.Entity3.TABLE_NAME)
+ assertThat(info.columns.size, `is`(2))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun failedForeignKey() {
+ val db = helper.createDatabase(TEST_DB, 6)
+ db.close()
+ var throwable: Throwable? = null
+ try {
+ helper.runMigrationsAndValidate(TEST_DB,
+ 7, false, object : Migration(6, 7) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE Entity4 (`id` INTEGER, `name` TEXT,"
+ + " PRIMARY KEY(`id`))")
+ }
+ })
+ } catch (t: Throwable) {
+ throwable = t
+ }
+
+ assertThat<Throwable>(throwable, instanceOf<Throwable>(IllegalStateException::class.java))
+
+ assertThat<String>(throwable!!.message, containsString("Migration failed"))
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun newTableWithForeignKey() {
+ helper.createDatabase(TEST_DB, 6)
+ val db = helper.runMigrationsAndValidate(TEST_DB,
+ 7, false, MIGRATION_6_7)
+ val info = TableInfo.read(db, MigrationDbKotlin.Entity4.TABLE_NAME)
+ assertThat(info.foreignKeys.size, `is`(1))
+ }
+
+ @Throws(IOException::class)
+ private fun testFailure(startVersion: Int, endVersion: Int) {
+ val db = helper.createDatabase(TEST_DB, startVersion)
+ db.close()
+ var throwable: Throwable? = null
+ try {
+ helper.runMigrationsAndValidate(TEST_DB, endVersion, true,
+ EmptyMigration(startVersion, endVersion))
+ } catch (t: Throwable) {
+ throwable = t
+ }
+
+ assertThat<Throwable>(throwable, instanceOf<Throwable>(IllegalStateException::class.java))
+ assertThat<String>(throwable!!.message, containsString("Migration failed"))
+ }
+
+ internal val MIGRATION_1_2: Migration = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` (`id` INTEGER NOT NULL,"
+ + " `name` TEXT, PRIMARY KEY(`id`))")
+ }
+ }
+
+ internal val MIGRATION_2_3: Migration = object : Migration(2, 3) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("ALTER TABLE " + MigrationDbKotlin.Entity2.TABLE_NAME
+ + " ADD COLUMN addedInV3 TEXT")
+ }
+ }
+
+ internal val MIGRATION_3_4: Migration = object : Migration(3, 4) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER NOT NULL,"
+ + " `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))")
+ }
+ }
+
+ internal val MIGRATION_4_5: Migration = object : Migration(4, 5) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER NOT NULL,"
+ + " `name` TEXT, PRIMARY KEY(`id`))")
+ database.execSQL("INSERT INTO Entity3_New(`id`, `name`) "
+ + "SELECT `id`, `name` FROM Entity3")
+ database.execSQL("DROP TABLE Entity3")
+ database.execSQL("ALTER TABLE Entity3_New RENAME TO Entity3")
+ }
+ }
+
+ internal val MIGRATION_5_6: Migration = object : Migration(5, 6) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("DROP TABLE " + MigrationDbKotlin.Entity3.TABLE_NAME)
+ }
+ }
+
+ internal val MIGRATION_6_7: Migration = object : Migration(6, 7) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS "
+ + MigrationDbKotlin.Entity4.TABLE_NAME
+ + " (`id` INTEGER NOT NULL, `name` TEXT, 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 "
+ + MigrationDbKotlin.Entity1.TABLE_NAME + " (`name`)")
+ }
+ }
+
+ private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
+ MIGRATION_5_6, MIGRATION_6_7)
+
+ internal class EmptyMigration(startVersion: Int, endVersion: Int)
+ : Migration(startVersion, endVersion) {
+
+ override fun migrate(database: SupportSQLiteDatabase) {
+ // do nothing
+ }
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
new file mode 100644
index 0000000..d73bc96
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -0,0 +1,323 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.test
+
+import android.database.sqlite.SQLiteConstraintException
+import android.support.test.filters.SmallTest
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.room.integration.kotlintestapp.vo.Author
+import androidx.room.integration.kotlintestapp.vo.Book
+import androidx.room.integration.kotlintestapp.vo.BookWithPublisher
+import androidx.room.integration.kotlintestapp.vo.Lang
+import androidx.room.integration.kotlintestapp.vo.Publisher
+import com.google.common.base.Optional
+import io.reactivex.Flowable
+import io.reactivex.schedulers.Schedulers
+import io.reactivex.subscribers.TestSubscriber
+import org.hamcrest.CoreMatchers
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import java.util.Date
+
+@SmallTest
+class BooksDaoTest : TestDatabaseTest() {
+
+ @Test
+ fun bookById() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ assertThat(booksDao.getBook(TestUtil.BOOK_1.bookId), `is`<Book>(TestUtil.BOOK_1))
+ }
+
+ @Test
+ fun bookByIdJavaOptional() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ assertThat(
+ booksDao.getBookJavaOptional(TestUtil.BOOK_1.bookId),
+ `is`<java.util.Optional<Book>>(java.util.Optional.of(TestUtil.BOOK_1)))
+ }
+
+ @Test
+ fun bookByIdJavaOptionalEmpty() {
+ assertThat(
+ booksDao.getBookJavaOptional(TestUtil.BOOK_1.bookId),
+ `is`<java.util.Optional<Book>>(java.util.Optional.empty()))
+ }
+
+ @Test
+ fun bookByIdListenableFuture() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ assertThat(
+ booksDao.getBookListenableFuture(TestUtil.BOOK_1.bookId).get(),
+ `is`<Book>(TestUtil.BOOK_1))
+ }
+
+ @Test
+ fun bookByIdOptional() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ assertThat(
+ booksDao.getBookOptional(TestUtil.BOOK_1.bookId),
+ `is`<Optional<Book>>(Optional.of(TestUtil.BOOK_1)))
+ }
+
+ @Test
+ fun bookByIdOptionalListenableFuture() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ assertThat(
+ booksDao.getBookOptionalListenableFuture(TestUtil.BOOK_1.bookId).get(),
+ `is`<Optional<Book>>(Optional.of(TestUtil.BOOK_1)))
+ }
+
+ @Test
+ fun bookByIdOptionalListenableFutureAbsent() {
+ assertThat(
+ booksDao.getBookOptionalListenableFuture(TestUtil.BOOK_1.bookId).get(),
+ `is`<Optional<Book>>(Optional.absent()))
+ }
+
+ @Test
+ fun bookByIdOptionalAbsent() {
+ assertThat(
+ booksDao.getBookOptional(TestUtil.BOOK_1.bookId),
+ `is`<Optional<Book>>(Optional.absent()))
+ }
+
+ @Test
+ fun bookByIdOptionalFlowable() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ val subscriber = TestSubscriber<Optional<Book>>()
+ val flowable: Flowable<Optional<Book>> =
+ booksDao.getBookOptionalFlowable(TestUtil.BOOK_1.bookId)
+ flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(subscriber)
+
+ assertThat(subscriber.values().size, `is`(1))
+ assertThat(subscriber.values()[0], `is`(Optional.of(TestUtil.BOOK_1)))
+ }
+
+ @Test
+ fun bookByIdOptionalFlowableAbsent() {
+ val subscriber = TestSubscriber<Optional<Book>>()
+ val flowable: Flowable<Optional<Book>> =
+ booksDao.getBookOptionalFlowable(TestUtil.BOOK_1.bookId)
+ flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(subscriber)
+
+ assertThat(subscriber.values().size, `is`(1))
+ assertThat(subscriber.values()[0], `is`(Optional.absent()))
+ }
+
+ @Test
+ fun bookWithPublisher() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ val expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
+ TestUtil.PUBLISHER)
+ val expectedList = ArrayList<BookWithPublisher>()
+ expectedList.add(expected)
+
+ assertThat(database.booksDao().getBooksWithPublisher(),
+ `is`<List<BookWithPublisher>>(expectedList))
+ }
+
+ @Test
+ fun bookWithPublisherListenableFuture() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ val expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
+ TestUtil.PUBLISHER)
+ val expectedList = ArrayList<BookWithPublisher>()
+ expectedList.add(expected)
+
+ assertThat(database.booksDao().getBooksWithPublisherListenableFuture().get(),
+ `is`<List<BookWithPublisher>>(expectedList))
+ }
+
+ @Test
+ fun updateBookWithNullTitle() {
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ var throwable: Throwable? = null
+ try {
+ booksDao.updateBookTitle(TestUtil.BOOK_1.bookId, null)
+ } catch (t: Throwable) {
+ throwable = t
+ }
+ assertNotNull(throwable)
+ assertThat<Throwable>(throwable, instanceOf<Throwable>(SQLiteConstraintException::class
+ .java))
+ }
+
+ @Test
+ fun publisherWithBooks() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
+
+ val actualPublisherWithBooks = booksDao.getPublisherWithBooks(
+ TestUtil.PUBLISHER.publisherId)
+
+ assertThat(actualPublisherWithBooks.publisher, `is`<Publisher>(TestUtil.PUBLISHER))
+ assertThat(actualPublisherWithBooks.books?.size, `is`(2))
+ assertThat(actualPublisherWithBooks.books?.get(0), `is`<Book>(TestUtil.BOOK_1))
+ assertThat(actualPublisherWithBooks.books?.get(1), `is`<Book>(TestUtil.BOOK_2))
+ }
+
+ @Test // b/68077506
+ fun publisherWithBookSales() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
+ val actualPublisherWithBooks = booksDao.getPublisherWithBookSales(
+ TestUtil.PUBLISHER.publisherId)
+
+ assertThat(actualPublisherWithBooks.publisher, `is`<Publisher>(TestUtil.PUBLISHER))
+ assertThat(actualPublisherWithBooks.sales, `is`(listOf(TestUtil.BOOK_1.salesCnt,
+ TestUtil.BOOK_2.salesCnt)))
+ }
+
+ @Test
+ fun insertAuthorWithAllFields() {
+ val author = Author("id", "name", Date(), ArrayList())
+ database.booksDao().addAuthors(author)
+
+ val authorDb = database.booksDao().getAuthor(author.authorId)
+
+ assertThat(authorDb, CoreMatchers.`is`<Author>(author))
+ }
+
+ @Test
+ fun insertInInheritedDao() {
+ database.derivedDao().insert(TestUtil.AUTHOR_1)
+
+ val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
+
+ assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
+ }
+
+ @Test
+ fun findBooksInMultiLineQuery() {
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+ booksDao.addBooks(TestUtil.BOOK_2)
+
+ val books = database.booksDao().getBooksMultiLineQuery(arrayListOf(
+ TestUtil.BOOK_1.bookId,
+ TestUtil.BOOK_2.bookId))
+ assertThat(books, `is`(listOf(TestUtil.BOOK_2, TestUtil.BOOK_1)))
+ }
+
+ @Test
+ fun findBooksByLanguage() {
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ val book1 = TestUtil.BOOK_1.copy(languages = setOf(Lang.TR))
+ val book2 = TestUtil.BOOK_2.copy(languages = setOf(Lang.ES, Lang.TR))
+ val book3 = TestUtil.BOOK_3.copy(languages = setOf(Lang.EN))
+ booksDao.addBooks(book1, book2, book3)
+
+ assertThat(booksDao.findByLanguages(setOf(Lang.EN, Lang.TR)),
+ `is`(listOf(book1, book2, book3)))
+
+ assertThat(booksDao.findByLanguages(setOf(Lang.TR)),
+ `is`(listOf(book1, book2)))
+
+ assertThat(booksDao.findByLanguages(setOf(Lang.ES)),
+ `is`(listOf(book2)))
+
+ assertThat(booksDao.findByLanguages(setOf(Lang.EN)),
+ `is`(listOf(book3)))
+ }
+
+ @Test
+ fun insertVarargInInheritedDao() {
+ database.derivedDao().insertAllArg(TestUtil.AUTHOR_1, TestUtil.AUTHOR_2)
+
+ val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
+
+ assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
+ }
+
+ @Test
+ fun insertListInInheritedDao() {
+ database.derivedDao().insertAll(listOf(TestUtil.AUTHOR_1))
+
+ val author = database.derivedDao().getAuthor(TestUtil.AUTHOR_1.authorId)
+
+ assertThat(author, CoreMatchers.`is`<Author>(TestUtil.AUTHOR_1))
+ }
+
+ @Test
+ fun deleteAndAddPublisher() {
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.getPublishers().run {
+ assertThat(this.size, `is`(1))
+ assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER)))
+ }
+ booksDao.deleteAndAddPublisher(TestUtil.PUBLISHER, TestUtil.PUBLISHER2)
+ booksDao.getPublishers().run {
+ assertThat(this.size, `is`(1))
+ assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER2)))
+ }
+ }
+
+ @Test
+ fun deleteAndAddPublisher_failure() {
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.getPublishers().run {
+ assertThat(this.size, `is`(1))
+ assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER)))
+ }
+ var throwable: Throwable? = null
+ try {
+ booksDao.deleteAndAddPublisher(TestUtil.PUBLISHER, TestUtil.PUBLISHER2, true)
+ } catch (e: RuntimeException) {
+ throwable = e
+ }
+ assertThat(throwable, `is`(notNullValue()))
+ booksDao.getPublishers().run {
+ assertThat(this.size, `is`(1))
+ assertThat(this.first(), `is`(equalTo(TestUtil.PUBLISHER)))
+ }
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
new file mode 100644
index 0000000..300695f
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/DependencyDaoTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2018 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 androidx.room.integration.kotlintestapp.test
+
+import android.os.Build
+import android.support.test.filters.SdkSuppress
+import android.support.test.runner.AndroidJUnit4
+import androidx.room.integration.kotlintestapp.dao.DependencyDao
+import androidx.room.integration.kotlintestapp.vo.DataClassFromDependency
+import androidx.room.integration.kotlintestapp.vo.EmbeddedFromDependency
+import androidx.room.integration.kotlintestapp.vo.PojoFromDependency
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DependencyDaoTest : TestDatabaseTest() {
+ lateinit var dao: DependencyDao
+ @Before
+ fun init() {
+ dao = database.dependencyDao()
+ }
+
+ @Test
+ fun insertAndGet() {
+ val data = insertSample(3)
+ assertThat(dao.selectAll(), `is`(listOf(data)))
+ }
+
+ @Test
+ fun insertAndGetByQuery() {
+ val data = insertSample(3)
+ assertThat(dao.findById(3), `is`(data))
+ assertThat(dao.findById(5), `is`(nullValue()))
+ }
+
+ @Test
+ fun insertAndGetByQuery_embedded() {
+ val data = insertSample(3)
+ assertThat(dao.findEmbedded(3), `is`(EmbeddedFromDependency(data)))
+ assertThat(dao.findEmbedded(5), `is`(nullValue()))
+ }
+
+ @Test
+ fun insertAndGetByQuery_pojo() {
+ val data = insertSample(3)
+ assertThat(dao.findPojo(3), `is`(PojoFromDependency(
+ id = data.id,
+ name = data.name)))
+ assertThat(dao.findPojo(5), `is`(nullValue()))
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ fun getRelation() {
+ val foo1 = DataClassFromDependency(
+ id = 3,
+ name = "foo"
+ )
+ val foo2 = DataClassFromDependency(
+ id = 4,
+ name = "foo"
+ )
+ val bar = DataClassFromDependency(
+ id = 5,
+ name = "bar"
+ )
+ dao.insert(foo1, foo2, bar)
+ val fooList = dao.relation("foo")
+ assertThat(fooList.sharedName, `is`("foo"))
+ assertThat(fooList, `is`(notNullValue()))
+ assertThat(fooList.dataItems, `is`(listOf(foo1, foo2)))
+
+ val barList = dao.relation("bar")
+ assertThat(barList.sharedName, `is`("bar"))
+ assertThat(barList, `is`(notNullValue()))
+ assertThat(barList.dataItems, `is`(listOf(bar)))
+
+ val bazList = dao.relation("baz")
+ assertThat(bazList.sharedName, `is`("baz"))
+ assertThat(bazList, `is`(notNullValue()))
+ assertThat(bazList.dataItems, `is`(emptyList()))
+ }
+
+ private fun insertSample(id: Int): DataClassFromDependency {
+ val data = DataClassFromDependency(
+ id = id,
+ name = "foo"
+ )
+ dao.insert(data)
+ return data
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ItemWithNullableConstructor.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ItemWithNullableConstructor.kt
new file mode 100644
index 0000000..d5b787c
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ItemWithNullableConstructor.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 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 androidx.room.integration.kotlintestapp.test
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.runner.AndroidJUnit4
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.RoomWarnings
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ItemWithNullableConstructor {
+ lateinit var db: Db
+ @Before
+ fun initDb() {
+ db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ Db::class.java).build()
+ }
+
+ @After
+ fun closeDb() {
+ db.close()
+ }
+
+ @Test
+ fun insertWithNull() {
+ db.dao.insert(TestItem(null, null))
+ assertThat(db.dao.get(), `is`(TestItem(1, null)))
+ }
+
+ @Entity
+ data class TestItem(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long? = null,
+ val nullable: Boolean?
+ )
+
+ @Dao
+ interface TestDao {
+ @Insert
+ fun insert(testItem: TestItem)
+
+ @Query("SELECT * FROM TestItem LIMIT 1")
+ fun get(): TestItem?
+ }
+
+ @Database(
+ version = 1,
+ entities = [TestItem::class],
+ exportSchema = false
+ )
+ @SuppressWarnings(RoomWarnings.MISSING_SCHEMA_LOCATION)
+ abstract class Db : RoomDatabase() {
+ abstract val dao: TestDao
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataQueryTest.kt
new file mode 100644
index 0000000..f39cea5
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataQueryTest.kt
@@ -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 androidx.room.integration.kotlintestapp.test
+
+import android.support.test.filters.SmallTest
+import androidx.room.integration.kotlintestapp.vo.Book
+import androidx.room.integration.kotlintestapp.vo.BookWithPublisher
+import androidx.room.integration.kotlintestapp.vo.Publisher
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+
+@SmallTest
+class LiveDataQueryTest : TestDatabaseTest() {
+
+ @Test
+ fun observeBooksById() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ val book = LiveDataTestUtil.getValue(booksDao.getBookLiveData(TestUtil.BOOK_1.bookId))
+
+ assertThat(book, `is`<Book>(TestUtil.BOOK_1))
+ }
+
+ @Test
+ fun observeBooksWithPublisher() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ var expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
+ TestUtil.PUBLISHER)
+ var expectedList = ArrayList<BookWithPublisher>()
+ expectedList.add(expected)
+
+ val actual = LiveDataTestUtil.getValue(booksDao.getBooksWithPublisherLiveData())
+ assertThat(actual, `is`<List<BookWithPublisher>>(expectedList))
+ }
+
+ @Test
+ fun publisherWithBooks() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
+
+ var actualPublisherWithBooks = LiveDataTestUtil.getValue(
+ booksDao.getPublisherWithBooksLiveData(TestUtil.PUBLISHER.publisherId))
+
+ assertThat(actualPublisherWithBooks.publisher, `is`<Publisher>(TestUtil.PUBLISHER))
+ assertThat(actualPublisherWithBooks.books?.size, `is`(2))
+ assertThat(actualPublisherWithBooks.books?.get(0), `is`<Book>(TestUtil.BOOK_1))
+ assertThat(actualPublisherWithBooks.books?.get(1), `is`<Book>(TestUtil.BOOK_2))
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
new file mode 100644
index 0000000..6dbd2b4
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.test
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+
+object LiveDataTestUtil {
+
+ @Throws(InterruptedException::class)
+ fun <T> getValue(liveData: LiveData<T>): T {
+ val data = arrayOfNulls<Any>(1)
+ val observer = object : Observer<T> {
+ override fun onChanged(o: T?) {
+ data[0] = o
+ liveData.removeObserver(this)
+ }
+ }
+ liveData.observeForever(observer)
+
+ return data[0] as T
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
new file mode 100644
index 0000000..f62dc70
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/RxJava2QueryTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.test
+
+import android.support.test.filters.SmallTest
+import androidx.room.EmptyResultSetException
+import androidx.room.integration.kotlintestapp.vo.BookWithPublisher
+import org.junit.Test
+
+@SmallTest
+class RxJava2QueryTest : TestDatabaseTest() {
+
+ @Test
+ fun observeBooksById() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ booksDao.getBookFlowable(TestUtil.BOOK_1.bookId)
+ .test()
+ .assertValue { book -> book == TestUtil.BOOK_1 }
+ }
+
+ @Test
+ fun observeBooksByIdSingle() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ booksDao.getBookSingle(TestUtil.BOOK_1.bookId)
+ .test()
+ .assertComplete()
+ .assertValue { book -> book == TestUtil.BOOK_1 }
+ }
+
+ @Test
+ fun observeBooksByIdSingle_noBook() {
+ booksDao.getBookSingle("x")
+ .test()
+ .assertError(EmptyResultSetException::class.java)
+ }
+
+ @Test
+ fun observeBooksByIdMaybe() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ booksDao.getBookMaybe(TestUtil.BOOK_1.bookId)
+ .test()
+ .assertComplete()
+ .assertValue { book -> book == TestUtil.BOOK_1 }
+ }
+
+ @Test
+ fun observeBooksByIdMaybe_noBook() {
+ booksDao.getBookMaybe("x")
+ .test()
+ .assertComplete()
+ .assertNoErrors()
+ .assertNoValues()
+ }
+
+ @Test
+ fun observeBooksWithPublisher() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1)
+
+ var expected = BookWithPublisher(TestUtil.BOOK_1.bookId, TestUtil.BOOK_1.title,
+ TestUtil.PUBLISHER)
+ var expectedList = ArrayList<BookWithPublisher>()
+ expectedList.add(expected)
+
+ booksDao.getBooksWithPublisherFlowable()
+ .test()
+ .assertValue(expectedList)
+ }
+
+ @Test
+ fun publisherWithBooks() {
+ booksDao.addAuthors(TestUtil.AUTHOR_1)
+ booksDao.addPublishers(TestUtil.PUBLISHER)
+ booksDao.addBooks(TestUtil.BOOK_1, TestUtil.BOOK_2)
+
+ booksDao.getPublisherWithBooksFlowable(TestUtil.PUBLISHER.publisherId)
+ .test()
+ .assertValue {
+ it.publisher == TestUtil.PUBLISHER
+ && it.books?.size == 2
+ && it.books?.get(0) == TestUtil.BOOK_1
+ && it.books?.get(1) == TestUtil.BOOK_2
+ }
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/TestDatabaseTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/TestDatabaseTest.kt
new file mode 100644
index 0000000..0f213188
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/TestDatabaseTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.test
+
+import android.support.test.InstrumentationRegistry
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.integration.kotlintestapp.TestDatabase
+import androidx.room.integration.kotlintestapp.dao.BooksDao
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+
+abstract class TestDatabaseTest {
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ protected lateinit var database: TestDatabase
+ protected lateinit var booksDao: BooksDao
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
+ TestDatabase::class.java)
+ // allowing main thread queries, just for testing
+ .allowMainThreadQueries()
+ .build()
+
+ booksDao = database.booksDao()
+ }
+
+ @After
+ @Throws(Exception::class)
+ fun tearDown() {
+ database.close()
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/TestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/TestUtil.kt
new file mode 100644
index 0000000..dfb4841
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/TestUtil.kt
@@ -0,0 +1,46 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.test
+
+import androidx.room.integration.kotlintestapp.vo.Author
+import androidx.room.integration.kotlintestapp.vo.Book
+import androidx.room.integration.kotlintestapp.vo.BookAuthor
+import androidx.room.integration.kotlintestapp.vo.Lang
+import androidx.room.integration.kotlintestapp.vo.Publisher
+
+class TestUtil {
+
+ companion object {
+
+ val PUBLISHER = Publisher("ph1", "publisher 1")
+ val PUBLISHER2 = Publisher("ph2", "publisher 2")
+
+ val AUTHOR_1 = Author("a1", "author 1")
+ val AUTHOR_2 = Author("a2", "author 2")
+
+ val BOOK_1 = Book("b1", "book title 1", "ph1",
+ setOf(Lang.EN), 3)
+ val BOOK_2 = Book("b2", "book title 2", "ph1",
+ setOf(Lang.TR), 5)
+ val BOOK_3 = Book("b3", "book title 2", "ph1",
+ setOf(Lang.ES), 7)
+
+ val BOOK_AUTHOR_1_1 = BookAuthor(BOOK_1.bookId, AUTHOR_1.authorId)
+ val BOOK_AUTHOR_1_2 = BookAuthor(BOOK_1.bookId, AUTHOR_2.authorId)
+ val BOOK_AUTHOR_2_2 = BookAuthor(BOOK_2.bookId, AUTHOR_2.authorId)
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Author.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Author.kt
new file mode 100644
index 0000000..6ee662d
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Author.kt
@@ -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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverters
+import java.util.Date
+
+@Entity
+@TypeConverters(DateConverter::class, StringToIntListConverters::class)
+data class Author(
+ @PrimaryKey val authorId: String,
+ val name: String,
+ val dateOfBirth: Date? = null,
+ val aList: List<Integer>? = null)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Book.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Book.kt
new file mode 100644
index 0000000..7b0def9
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Book.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverters
+
+@Entity(foreignKeys = arrayOf(
+ ForeignKey(entity = Publisher::class,
+ parentColumns = arrayOf("publisherId"),
+ childColumns = arrayOf("bookPublisherId"),
+ deferred = true)))
+data class Book(
+ @PrimaryKey val bookId: String,
+ val title: String,
+ val bookPublisherId: String,
+ @field:TypeConverters(Lang::class)
+ val languages: Set<Lang>,
+ val salesCnt: Int)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/BookAuthor.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/BookAuthor.kt
new file mode 100644
index 0000000..0201429
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/BookAuthor.kt
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+
+@Entity(foreignKeys = arrayOf(
+ ForeignKey(entity = Book::class,
+ parentColumns = arrayOf("bookId"),
+ childColumns = arrayOf("bookId"),
+ onUpdate = ForeignKey.CASCADE,
+ onDelete = ForeignKey.CASCADE,
+ deferred = true),
+ ForeignKey(entity = Author::class,
+ parentColumns = arrayOf("authorId"),
+ childColumns = arrayOf("authorId"),
+ onUpdate = ForeignKey.CASCADE,
+ onDelete = ForeignKey.CASCADE,
+ deferred = true)),
+ primaryKeys = arrayOf("bookId", "authorId"))
+data class BookAuthor(val bookId: String, val authorId: String)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/BookWithPublisher.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/BookWithPublisher.kt
new file mode 100644
index 0000000..069a894
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/BookWithPublisher.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Embedded
+
+data class BookWithPublisher(val bookId: String, val title: String,
+ @Embedded val publisher: Publisher)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt
new file mode 100755
index 0000000..cfdeb87
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.TypeConverter
+
+import java.util.Date
+
+class DateConverter {
+ @TypeConverter
+ fun toDate(timestamp: Long?): Date? {
+ return if (timestamp == null) null else Date(timestamp)
+ }
+
+ @TypeConverter
+ fun toTimestamp(date: Date?): Long? {
+ return date?.time
+ }
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Lang.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Lang.kt
new file mode 100644
index 0000000..473f566
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Lang.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.TypeConverter
+
+/**
+ * An enum class which gets saved as a bit set in the database.
+ */
+enum class Lang {
+ TR,
+ EN,
+ ES;
+
+ companion object {
+ @JvmStatic
+ @TypeConverter
+ fun toInt(langs: Set<Lang>): Int {
+ return langs.fold(0) { left, lang ->
+ left.or(1 shl lang.ordinal)
+ }
+ }
+
+ @JvmStatic
+ @TypeConverter
+ fun toSet(value: Int): Set<Lang> {
+ return Lang.values().filter {
+ (1 shl it.ordinal).and(value) != 0
+ }.toSet()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/NoArgClass.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/NoArgClass.kt
new file mode 100644
index 0000000..335de5b
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/NoArgClass.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * just here to ensure that we handle no-arg constructors fine from kotlin.
+ */
+@Entity
+data class NoArgClass(@PrimaryKey var id: Long = 0, var class_name: String = "")
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Publisher.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Publisher.kt
new file mode 100644
index 0000000..c38a823
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Publisher.kt
@@ -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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class Publisher(@PrimaryKey val publisherId: String, val name: String)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/PublisherWithBookSales.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/PublisherWithBookSales.kt
new file mode 100644
index 0000000..4b1fdde
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/PublisherWithBookSales.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+data class PublisherWithBookSales @JvmOverloads constructor(
+ @Embedded
+ val publisher: Publisher,
+ @Relation(parentColumn = "publisherId", // publisher.publisherId
+ entityColumn = "bookPublisherId", // book.bookPublisherId
+ entity = Book::class,
+ projection = ["salesCnt"])
+ var sales: List<Int>? = emptyList())
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/PublisherWithBooks.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/PublisherWithBooks.kt
new file mode 100644
index 0000000..8a6e0d1
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/PublisherWithBooks.kt
@@ -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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+class PublisherWithBooks {
+ @Embedded var publisher: Publisher? = null
+ @Relation(parentColumn = "publisherId", // publisher.publisherId
+ entityColumn = "bookPublisherId", // book.bookPublisherId
+ entity = Book::class)
+ var books: List<Book>? = null
+}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/StringToIntListConverters.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/StringToIntListConverters.kt
new file mode 100644
index 0000000..38f552a
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/StringToIntListConverters.kt
@@ -0,0 +1,18 @@
+package androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.TypeConverter
+import androidx.room.util.StringUtil
+
+object StringToIntListConverters {
+ @TypeConverter
+ // Specifying that a static method should be generated. Otherwise, the compiler looks for the
+ // constructor of the class, and a object has a private constructor.
+ @JvmStatic
+ fun stringToIntList(data: String?): List<Int>? =
+ if (data == null) null else StringUtil.splitToIntList(data)
+
+ @TypeConverter
+ @JvmStatic
+ fun intListToString(ints: List<Int>?): String? =
+ if (ints == null) null else StringUtil.joinIntoString(ints)
+}
diff --git a/room/integration-tests/kotlintestapp/src/main/AndroidManifest.xml b/room/integration-tests/kotlintestapp/src/main/AndroidManifest.xml
index d079891..690eeb5 100644
--- a/room/integration-tests/kotlintestapp/src/main/AndroidManifest.xml
+++ b/room/integration-tests/kotlintestapp/src/main/AndroidManifest.xml
@@ -15,4 +15,4 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.room.integration.kotlintestapp"/>
+ package="androidx.room.integration.kotlintestapp"/>
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/dao/DependencyDao.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/dao/DependencyDao.kt
deleted file mode 100644
index 90e45cf..0000000
--- a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/dao/DependencyDao.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2018 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.kotlintestapp.dao
-
-import android.arch.persistence.room.Dao
-import android.arch.persistence.room.Insert
-import android.arch.persistence.room.OnConflictStrategy
-import android.arch.persistence.room.Query
-import android.arch.persistence.room.integration.kotlintestapp.vo.DataClassFromDependency
-import android.arch.persistence.room.integration.kotlintestapp.vo.EmbeddedFromDependency
-import android.arch.persistence.room.integration.kotlintestapp.vo.PojoFromDependency
-import android.arch.persistence.room.integration.kotlintestapp.vo.RelationFromDependency
-import android.os.Build
-import android.support.annotation.RequiresApi
-
-@Dao
-interface DependencyDao {
- @Query("select * from DataClassFromDependency")
- fun selectAll(): List<DataClassFromDependency>
-
- @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
- fun findEmbedded(id: Int): EmbeddedFromDependency
-
- @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
- fun findPojo(id: Int): PojoFromDependency
-
- @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
- fun findById(id: Int): DataClassFromDependency
-
- @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
- @Query("WITH nameTable( sharedName ) AS ( SELECT :name ) SELECT * from nameTable")
- fun relation(name: String): RelationFromDependency
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(vararg input: DataClassFromDependency)
-}
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/DataClassFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/DataClassFromDependency.kt
deleted file mode 100644
index 258ada6..0000000
--- a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/DataClassFromDependency.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2018 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Entity
-import android.arch.persistence.room.PrimaryKey
-
-/**
- * used to test the case where kotlin classes from dependencies cannot be read properly.
- * Since the main db in this app is in the test module, the original classes serve as a dependency.
- */
-@Entity
-data class DataClassFromDependency(
- @PrimaryKey(autoGenerate = true)
- val id: Int,
- val name: String)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/EmbeddedFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/EmbeddedFromDependency.kt
deleted file mode 100644
index e1ed9be..0000000
--- a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/EmbeddedFromDependency.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2018 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Embedded
-
-data class EmbeddedFromDependency(
- @Embedded
- val data: DataClassFromDependency)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/PojoFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/PojoFromDependency.kt
deleted file mode 100644
index f2cc3ad..0000000
--- a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/PojoFromDependency.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2018 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.kotlintestapp.vo
-
-data class PojoFromDependency(
- val id: Int,
- val name: String)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/RelationFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/RelationFromDependency.kt
deleted file mode 100644
index 5fe7d4b..0000000
--- a/room/integration-tests/kotlintestapp/src/main/java/android/arch/persistence/room/integration/kotlintestapp/vo/RelationFromDependency.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2018 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.kotlintestapp.vo
-
-import android.arch.persistence.room.Relation
-
-class RelationFromDependency(val sharedName: String) {
- @Relation(
- parentColumn = "sharedName",
- entityColumn = "name"
- )
- lateinit var dataItems: List<DataClassFromDependency>
-}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
new file mode 100644
index 0000000..e5e8eb6
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/dao/DependencyDao.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 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 androidx.room.integration.kotlintestapp.dao
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.integration.kotlintestapp.vo.DataClassFromDependency
+import androidx.room.integration.kotlintestapp.vo.EmbeddedFromDependency
+import androidx.room.integration.kotlintestapp.vo.PojoFromDependency
+import androidx.room.integration.kotlintestapp.vo.RelationFromDependency
+
+@Dao
+interface DependencyDao {
+ @Query("select * from DataClassFromDependency")
+ fun selectAll(): List<DataClassFromDependency>
+
+ @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
+ fun findEmbedded(id: Int): EmbeddedFromDependency
+
+ @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
+ fun findPojo(id: Int): PojoFromDependency
+
+ @Query("select * from DataClassFromDependency where id = :id LIMIT 1")
+ fun findById(id: Int): DataClassFromDependency
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ @Query("WITH nameTable( sharedName ) AS ( SELECT :name ) SELECT * from nameTable")
+ fun relation(name: String): RelationFromDependency
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg input: DataClassFromDependency)
+}
diff --git a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/DataClassFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/DataClassFromDependency.kt
new file mode 100644
index 0000000..6dd8307
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/DataClassFromDependency.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * used to test the case where kotlin classes from dependencies cannot be read properly.
+ * Since the main db in this app is in the test module, the original classes serve as a dependency.
+ */
+@Entity
+data class DataClassFromDependency(
+ @PrimaryKey(autoGenerate = true)
+ val id: Int,
+ val name: String)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/EmbeddedFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/EmbeddedFromDependency.kt
new file mode 100644
index 0000000..2d1c46e
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/EmbeddedFromDependency.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Embedded
+
+data class EmbeddedFromDependency(
+ @Embedded
+ val data: DataClassFromDependency)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/PojoFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/PojoFromDependency.kt
new file mode 100644
index 0000000..fee9838
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/PojoFromDependency.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.kotlintestapp.vo
+
+data class PojoFromDependency(
+ val id: Int,
+ val name: String)
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/RelationFromDependency.kt b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/RelationFromDependency.kt
new file mode 100644
index 0000000..8f0b48b
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/main/java/androidx/room/integration/kotlintestapp/vo/RelationFromDependency.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Relation
+
+class RelationFromDependency(val sharedName: String) {
+ @Relation(
+ parentColumn = "sharedName",
+ entityColumn = "name"
+ )
+ lateinit var dataItems: List<DataClassFromDependency>
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index 401cc07..22fd802 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -34,30 +34,30 @@
}
dependencies {
- implementation(project(":room:common"))
- implementation(project(":persistence:db"))
- implementation(project(":persistence:db-framework"))
- implementation(project(":room:runtime"))
- implementation(project(":arch:runtime"))
- implementation(project(":arch:common"))
- implementation(project(":paging:common"))
- implementation(project(":lifecycle:extensions"))
- implementation(project(":lifecycle:runtime"))
- implementation(project(":lifecycle:common"))
- implementation(project(":room:rxjava2"))
- implementation(project(":paging:runtime"))
+ implementation(project(":room:room-common"))
+ implementation(project(":sqlite:sqlite"))
+ implementation(project(":sqlite:sqlite-framework"))
+ implementation(project(":room:room-runtime"))
+ implementation(project(":arch:core-runtime"))
+ implementation(project(":arch:core-common"))
+ implementation(project(":paging:paging-common"))
+ implementation(project(":lifecycle:lifecycle-extensions"))
+ implementation(project(":lifecycle:lifecycle-runtime"))
+ implementation(project(":lifecycle:lifecycle-common"))
+ implementation(project(":room:room-rxjava2"))
+ implementation(project(":paging:paging-runtime"))
- implementation(SUPPORT_RECYCLERVIEW_27, libs.support_exclude_config)
- implementation(SUPPORT_APPCOMPAT_27, libs.support_exclude_config)
- annotationProcessor project(":room:compiler")
- androidTestAnnotationProcessor project(":room:compiler")
+ implementation(SUPPORT_RECYCLERVIEW, libs.support_exclude_config)
+ implementation(SUPPORT_APPCOMPAT, libs.support_exclude_config)
+ annotationProcessor project(":room:room-compiler")
+ androidTestAnnotationProcessor project(":room:room-compiler")
// IJ's gradle integration just cannot figure this out ...
- androidTestImplementation(project(":lifecycle:extensions"))
- androidTestImplementation(project(":lifecycle:common"))
- androidTestImplementation(project(":lifecycle:runtime"))
- androidTestImplementation(project(":room:testing"))
- androidTestImplementation(project(":room:rxjava2"))
+ androidTestImplementation(project(":lifecycle:lifecycle-extensions"))
+ androidTestImplementation(project(":lifecycle:lifecycle-common"))
+ androidTestImplementation(project(":lifecycle:lifecycle-runtime"))
+ androidTestImplementation(project(":room:room-testing"))
+ androidTestImplementation(project(":room:room-rxjava2"))
androidTestImplementation(project(":arch:core-testing"))
androidTestImplementation(RX_JAVA)
androidTestImplementation(TEST_RUNNER)
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/1.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/1.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/1.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/1.json
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/2.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/2.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/2.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/2.json
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/3.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/3.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/3.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/3.json
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/4.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/4.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/4.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/4.json
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/5.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/5.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/5.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/5.json
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/6.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/6.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/6.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/6.json
diff --git a/room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/7.json b/room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/7.json
similarity index 100%
rename from room/integration-tests/testapp/schemas/android.arch.persistence.room.integration.testapp.migration.MigrationDb/7.json
rename to room/integration-tests/testapp/schemas/androidx.room.integration.testapp.migration.MigrationDb/7.json
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/InvalidationTrackerTrojan.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/InvalidationTrackerTrojan.java
deleted file mode 100644
index 5fa15ac..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/InvalidationTrackerTrojan.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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;
-
-/**
- * Trojan class to be able to assert internal state.
- */
-public class InvalidationTrackerTrojan {
- public static int countObservers(InvalidationTracker tracker) {
- return tracker.mObserverMap.size();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java
deleted file mode 100644
index 63b9507..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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;
-
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.Query;
-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,
- 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
- public interface IntPKeyDao {
- @Insert
- void insertMe(IntAutoIncPKeyEntity... items);
- @Insert
- long insertAndGetId(IntAutoIncPKeyEntity item);
-
- @Insert
- long[] insertAndGetIds(IntAutoIncPKeyEntity... item);
-
- @Query("select * from IntAutoIncPKeyEntity WHERE pKey = :key")
- IntAutoIncPKeyEntity getMe(int key);
-
- @Query("select data from IntAutoIncPKeyEntity WHERE pKey IN(:ids)")
- List<String> loadDataById(long... ids);
- }
-
- @Dao
- public interface IntegerAutoIncPKeyDao {
- @Insert
- void insertMe(IntegerAutoIncPKeyEntity item);
-
- @Query("select * from IntegerAutoIncPKeyEntity WHERE pKey = :key")
- IntegerAutoIncPKeyEntity getMe(int key);
-
- @Insert
- long insertAndGetId(IntegerAutoIncPKeyEntity item);
-
- @Insert
- long[] insertAndGetIds(IntegerAutoIncPKeyEntity... item);
-
- @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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
deleted file mode 100644
index 98282ab..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-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 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;
-import android.arch.persistence.room.integration.testapp.dao.RawDao;
-import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
-import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
-import android.arch.persistence.room.integration.testapp.dao.ToyDao;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-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.Day;
-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;
-import android.arch.persistence.room.integration.testapp.vo.School;
-import android.arch.persistence.room.integration.testapp.vo.Toy;
-import android.arch.persistence.room.integration.testapp.vo.User;
-
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Set;
-
-@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
- BlobEntity.class, Product.class, FunnyNamedEntity.class},
- version = 1, exportSchema = false)
-@TypeConverters(TestDatabase.Converters.class)
-public abstract class TestDatabase extends RoomDatabase {
- public abstract UserDao getUserDao();
- public abstract PetDao getPetDao();
- public abstract UserPetDao getUserPetDao();
- public abstract SchoolDao getSchoolDao();
- public abstract PetCoupleDao getPetCoupleDao();
- public abstract ToyDao getToyDao();
- public abstract BlobEntityDao getBlobEntityDao();
- public abstract ProductDao getProductDao();
- public abstract SpecificDogDao getSpecificDogDao();
- public abstract WithClauseDao getWithClauseDao();
- public abstract FunnyNamedDao getFunnyNamedDao();
- public abstract RawDao getRawDao();
-
- @SuppressWarnings("unused")
- public static class Converters {
- @TypeConverter
- public Date fromTimestamp(Long value) {
- return value == null ? null : new Date(value);
- }
-
- @TypeConverter
- public Long dateToTimestamp(Date date) {
- if (date == null) {
- return null;
- } else {
- return date.getTime();
- }
- }
-
- @TypeConverter
- public Set<Day> decomposeDays(int flags) {
- Set<Day> result = new HashSet<>();
- for (Day day : Day.values()) {
- if ((flags & (1 << day.ordinal())) != 0) {
- result.add(day);
- }
- }
- return result;
- }
-
- @TypeConverter
- public int composeDays(Set<Day> days) {
- int result = 0;
- for (Day day : days) {
- result |= 1 << day.ordinal();
- }
- return result;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/BlobEntityDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/BlobEntityDao.java
deleted file mode 100644
index 212eba7..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/BlobEntityDao.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
-
-import java.util.List;
-
-@Dao
-public interface BlobEntityDao {
-
- @Insert
- void insert(BlobEntity item);
-
- @Query("SELECT * FROM BlobEntity")
- List<BlobEntity> selectAll();
-
- @Query("SELECT content FROM BlobEntity WHERE id = :id")
- byte[] getContent(long id);
-
- @Query("UPDATE BlobEntity SET content = :content WHERE id = :id")
- void updateContent(long id, byte[] content);
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java
deleted file mode 100644
index 93b5e72..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/PetCoupleDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/PetCoupleDao.java
deleted file mode 100644
index 4f7c4e2..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/PetCoupleDao.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.integration.testapp.vo.PetCouple;
-
-import java.util.List;
-
-@Dao
-public interface PetCoupleDao {
- @Insert
- void insert(PetCouple couple);
-
- @Query("SELECT * FROM PetCouple")
- List<PetCouple> loadAll();
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/PetDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/PetDao.java
deleted file mode 100644
index e3a45a0..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/PetDao.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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 android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Delete;
-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.integration.testapp.vo.Pet;
-import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
-
-import java.util.List;
-
-@Dao
-public interface PetDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- void insertOrReplace(Pet... pets);
-
- @Insert
- void insertAll(Pet[] pets);
-
- @Query("SELECT COUNT(*) FROM Pet")
- int count();
-
- @Query("SELECT * FROM Pet ORDER BY Pet.mPetId ASC")
- List<PetWithToyIds> allPetsWithToyIds();
-
- @Delete
- void delete(Pet pet);
-
- @Query("SELECT mPetId FROM Pet")
- int[] allIds();
-
- @Transaction
- default void deleteAndInsert(Pet oldPet, Pet newPet, boolean shouldFail) {
- delete(oldPet);
- if (shouldFail) {
- throw new RuntimeException();
- }
- insertOrReplace(newPet);
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/ProductDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/ProductDao.java
deleted file mode 100644
index 417fb72..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/ProductDao.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.integration.testapp.vo.Product;
-import android.support.annotation.NonNull;
-
-@Dao
-public interface ProductDao {
-
- @Insert
- long insert(@NonNull Product product);
-
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/RawDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/RawDao.java
deleted file mode 100644
index ae09350..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/RawDao.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2018 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 android.arch.lifecycle.LiveData;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.room.ColumnInfo;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.RawQuery;
-import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
-import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
-
-import java.util.Date;
-import java.util.List;
-
-@Dao
-public interface RawDao {
- @RawQuery
- User getUser(String query);
-
- @RawQuery
- UserAndAllPets getUserAndAllPets(String query);
-
- @RawQuery(observedEntities = UserAndAllPets.class)
- LiveData<UserAndAllPets> getUserAndAllPetsObservable(String query);
-
- @RawQuery
- User getUser(SupportSQLiteQuery query);
-
- @RawQuery
- UserAndPet getUserAndPet(String query);
-
- @RawQuery
- NameAndLastName getUserNameAndLastName(String query);
-
- @RawQuery(observedEntities = User.class)
- NameAndLastName getUserNameAndLastName(SupportSQLiteQuery query);
-
- @RawQuery
- int count(String query);
-
- @RawQuery
- List<User> getUserList(String query);
-
- @RawQuery
- List<UserAndPet> getUserAndPetList(String query);
-
- @RawQuery(observedEntities = UserAndPet.class)
- LiveData<List<UserAndPet>> getUserAndPetListObservable(String query);
-
- @RawQuery(observedEntities = User.class)
- LiveData<User> getUserLiveData(String query);
-
- @RawQuery(observedEntities = User.class)
- LiveData<User> getUserLiveData(SupportSQLiteQuery query);
-
- @RawQuery
- UserNameAndBirthday getUserAndBirthday(String query);
-
- class UserNameAndBirthday {
- @ColumnInfo(name = "mName")
- public final String name;
- @ColumnInfo(name = "mBirthday")
- public final Date birthday;
-
- public UserNameAndBirthday(String name, Date birthday) {
- this.name = name;
- this.birthday = birthday;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java
deleted file mode 100644
index 18e8d93..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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 android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.OnConflictStrategy;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.RoomWarnings;
-import android.arch.persistence.room.integration.testapp.vo.Coordinates;
-import android.arch.persistence.room.integration.testapp.vo.School;
-import android.arch.persistence.room.integration.testapp.vo.SchoolRef;
-
-import java.util.List;
-
-@Dao
-public abstract class SchoolDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- public abstract void insert(School... schools);
-
- @Query("SELECT * from School WHERE address_street LIKE '%' || :street || '%'")
- public abstract List<School> findByStreet(String street);
-
- @Query("SELECT mId, mName, manager_mName FROM School")
- public abstract List<School> schoolAndManagerNames();
-
- @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 mId, address_lat, address_lng FROM School WHERE mId = :schoolId")
- public abstract School loadCoordinatesAsSchool(int schoolId);
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/SpecificDogDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/SpecificDogDao.java
deleted file mode 100644
index 971826d..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/SpecificDogDao.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 android.arch.lifecycle.LiveData;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.integration.testapp.vo.PetsToys;
-
-@Dao
-public interface SpecificDogDao {
- @Query("SELECT 123 as petId")
- LiveData<PetsToys> getSpecificDogsToys();
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/ToyDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/ToyDao.java
deleted file mode 100644
index 5d50a62..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/ToyDao.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.integration.testapp.vo.Toy;
-
-@Dao
-public interface ToyDao {
- @Insert
- void insert(Toy... toys);
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
deleted file mode 100644
index 1db5b46..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.arch.lifecycle.LiveData;
-import android.arch.paging.DataSource;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Delete;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.OnConflictStrategy;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.RawQuery;
-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;
-import android.arch.persistence.room.integration.testapp.vo.Day;
-import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.database.Cursor;
-
-import org.reactivestreams.Publisher;
-
-import java.util.Date;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.Callable;
-
-import io.reactivex.Flowable;
-import io.reactivex.Maybe;
-import io.reactivex.Single;
-
-@SuppressWarnings("SameParameterValue")
-@Dao
-public abstract class UserDao {
-
- private final TestDatabase mDatabase;
-
- public UserDao(TestDatabase database) {
- mDatabase = database;
- }
-
- @Query("select * from user where mName like :name")
- public abstract List<User> findUsersByName(String name);
-
- @Query("select * from user where mId = :id")
- public abstract User load(int id);
-
- @Query("select * from user where mId IN(:ids)")
- public abstract User[] loadByIds(int... ids);
-
- @Query("select * from user where custommm = :customField")
- public abstract List<User> findByCustomField(String customField);
-
- @Insert
- public abstract void insert(User user);
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- public abstract void insertOrReplace(User user);
-
- @Delete
- public abstract int delete(User user);
-
- @Delete
- public abstract int deleteAll(User[] users);
-
- @Query("delete from user")
- public abstract int deleteEverything();
-
- @Update
- public abstract int update(User user);
-
- @Update
- public abstract int updateAll(List<User> users);
-
- @Insert
- public abstract void insertAll(User[] users);
-
- @Query("select * from user where mAdmin = :isAdmin")
- public abstract List<User> findByAdmin(boolean isAdmin);
-
- @Query("delete from user where mAge > :age")
- public abstract int deleteAgeGreaterThan(int age);
-
- @Query("delete from user where mId IN(:uids)")
- public abstract int deleteByUids(int... uids);
-
- @Query("delete from user where mAge >= :min AND mAge <= :max")
- public abstract int deleteByAgeRange(int min, int max);
-
- @Query("update user set mName = :name where mId = :id")
- public abstract int updateById(int id, String name);
-
- @Query("update user set mId = mId + :amount")
- public abstract void incrementIds(int amount);
-
- @Query("update user set mAge = mAge + 1")
- public abstract void incrementAgeOfAll();
-
- @Query("select mId from user order by mId ASC")
- public abstract List<Integer> loadIds();
-
- @Query("select * from user where mId = :id")
- public abstract LiveData<User> liveUserById(int id);
-
- @Transaction
- @Query("select * from user where mId = :id")
- public abstract LiveData<User> liveUserByIdInTransaction(int id);
-
- @Query("select * from user where mName LIKE '%' || :name || '%' ORDER BY mId DESC")
- public abstract LiveData<List<User>> liveUsersListByName(String name);
-
- @Query("select * from user where length(mName) = :length")
- public abstract List<User> findByNameLength(int length);
-
- @Query("select * from user where mAge = :age")
- public abstract List<User> findByAge(int age);
-
- @Query("select mAge, AVG(mWeight) from user GROUP BY mAge ORDER BY 2 DESC")
- public abstract List<AvgWeightByAge> weightByAge();
-
- @Query("select mAge, AVG(mWeight) from user GROUP BY mAge ORDER BY 2 DESC LIMIT 1")
- public abstract LiveData<AvgWeightByAge> maxWeightByAgeGroup();
-
- @Query("select * from user where mBirthday > :from AND mBirthday < :to")
- public abstract List<User> findByBirthdayRange(Date from, Date to);
-
- @Query("select mId from user where mId IN (:ids)")
- public abstract Cursor findUsersAsCursor(int... ids);
-
- @Query("select * from user where mId = :id")
- public abstract Flowable<User> flowableUserById(int id);
-
- @Query("select * from user where mId = :id")
- public abstract Maybe<User> maybeUserById(int id);
-
- @Query("select * from user where mId IN (:ids)")
- public abstract Maybe<List<User>> maybeUsersByIds(int... ids);
-
- @Query("select * from user where mId = :id")
- public abstract Single<User> singleUserById(int id);
-
- @Query("select * from user where mId IN (:ids)")
- public abstract Single<List<User>> singleUsersByIds(int... ids);
-
- @Query("select COUNT(*) from user")
- public abstract Flowable<Integer> flowableCountUsers();
-
- @Query("select COUNT(*) from user")
- public abstract Publisher<Integer> publisherCountUsers();
-
- @Query("SELECT mBirthday from User where mId = :id")
- public abstract Date getBirthday(int id);
-
- @Query("SELECT COUNT(*) from user")
- public abstract int count();
-
- @Query("SELECT mAdmin from User where mId = :uid")
- public abstract boolean isAdmin(int uid);
-
- @Query("SELECT mAdmin from User where mId = :uid")
- public abstract LiveData<Boolean> isAdminLiveData(int uid);
-
- public void insertBothByRunnable(final User a, final User b) {
- mDatabase.runInTransaction(new Runnable() {
- @Override
- public void run() {
- insert(a);
- insert(b);
- }
- });
- }
-
- public int insertBothByCallable(final User a, final User b) {
- return mDatabase.runInTransaction(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- insert(a);
- insert(b);
- return 2;
- }
- });
- }
-
- @Query("SELECT * FROM user where mAge > :age")
- public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
-
- @RawQuery(observedEntities = User.class)
- public abstract DataSource.Factory<Integer, User> loadPagedByAgeWithObserver(
- SupportSQLiteQuery query);
-
- // TODO: switch to PositionalDataSource once Room supports it
- @Query("SELECT * FROM user ORDER BY mAge DESC")
- public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
-
- @Query("DELETE FROM User WHERE mId IN (:ids) AND mAge == :age")
- public abstract int deleteByAgeAndIds(int age, List<Integer> ids);
-
- @Query("UPDATE User set mWeight = :weight WHERE mId IN (:ids) AND mAge == :age")
- public abstract int updateByAgeAndIds(float weight, int age, List<Integer> ids);
-
- @Query("SELECT * FROM user WHERE (mWorkDays & :days) != 0")
- public abstract List<User> findUsersByWorkDays(Set<Day> days);
-
- // QueryLoader
-
- @Query("SELECT COUNT(*) from user")
- public abstract Integer getUserCount();
-
- @Query("SELECT u.mName, u.mLastName from user u where mId = :id")
- public abstract NameAndLastName getNameAndLastName(int id);
-
- @Transaction
- public void insertBothByAnnotation(final User a, final User b) {
- insert(a);
- insert(b);
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
deleted file mode 100644
index d7a0e1e..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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 android.arch.lifecycle.LiveData;
-import android.arch.paging.DataSource;
-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.EmbeddedUserAndAllPets;
-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.arch.persistence.room.integration.testapp.vo.UserAndPet;
-import android.arch.persistence.room.integration.testapp.vo.UserAndPetAdoptionDates;
-import android.arch.persistence.room.integration.testapp.vo.UserAndPetNonNull;
-import android.arch.persistence.room.integration.testapp.vo.UserIdAndPetNames;
-import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys;
-
-import java.util.List;
-
-import io.reactivex.Flowable;
-
-@Dao
-public interface UserPetDao {
- @Query("SELECT * FROM User u, Pet p WHERE u.mId = p.mUserId")
- List<UserAndPet> loadAll();
-
- @Query("SELECT * FROM User u LEFT OUTER JOIN Pet p ON u.mId = p.mUserId")
- List<UserAndPet> loadUsers();
-
- @Query("SELECT * FROM User u LEFT OUTER JOIN Pet p ON u.mId = p.mUserId")
- List<UserAndPetNonNull> loadUsersWithNonNullPet();
-
- @Query("SELECT * FROM Pet p LEFT OUTER JOIN User u ON u.mId = p.mUserId")
- List<UserAndPet> loadPets();
-
- @Query("SELECT * FROM User u LEFT OUTER JOIN Pet p ON u.mId = p.mUserId")
- DataSource.Factory<Integer, UserAndAllPets> dataSourceFactoryMultiTable();
-
- @Query("SELECT * FROM User u")
- List<UserAndAllPets> loadAllUsersWithTheirPets();
-
- @Query("SELECT * FROM User u")
- List<UserIdAndPetNames> loadUserAndPetNames();
-
- @Query("SELECT * FROM User u")
- List<UserWithPetsAndToys> loadUserWithPetsAndToys();
-
- @Query("SELECT * FROM User UNION ALL SELECT * FROM USER")
- List<UserAndAllPets> unionByItself();
-
- @Query("SELECT * FROM User")
- List<UserAndPetAdoptionDates> loadUserWithPetAdoptionDates();
-
- @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);
-
- @Insert
- void insertUserAndPet(User user, Pet pet);
-
- @Update
- void updateUsersAndPets(User[] users, Pet[] pets);
-
- @Delete
- void delete2UsersAndPets(User user1, User user2, Pet[] pets);
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java
deleted file mode 100644
index 40098ed..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 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"
- + " UNION ALL \n"
- + " SELECT n+1, (n+1)*fact FROM factorial \n"
- + " WHERE n < :maxIndexInclusive)\n"
- + " SELECT fact FROM factorial;")
- List<Integer> getFactorials(int maxIndexInclusive);
-
- @Query("WITH RECURSIVE factorial(n, fact) AS \n"
- + "(SELECT 0, 1 \n"
- + " UNION ALL \n"
- + " SELECT n+1, (n+1)*fact FROM factorial \n"
- + " WHERE n < :maxIndexInclusive)\n"
- + " SELECT mName FROM User WHERE User.mId IN (Select fact FROM factorial);")
- List<String> getUsersWithFactorialIds(int maxIndexInclusive);
-
- @Query("WITH RECURSIVE factorial(n, fact) AS \n"
- + "(SELECT 0, 1 \n"
- + " UNION ALL \n"
- + " SELECT n+1, (n+1)*fact FROM factorial \n"
- + " WHERE n < :maxIndexInclusive)\n"
- + " SELECT mName FROM User WHERE User.mId IN (Select fact FROM factorial);")
- LiveData<List<String>> getUsersWithFactorialIdsLiveData(int maxIndexInclusive);
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java
deleted file mode 100644
index ef207cf..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationDb.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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.migration;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.room.ColumnInfo;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.ForeignKey;
-import android.arch.persistence.room.Ignore;
-import android.arch.persistence.room.Index;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.RoomDatabase;
-import android.content.ContentValues;
-import android.database.sqlite.SQLiteDatabase;
-
-import java.util.List;
-
-@SuppressWarnings("WeakerAccess")
-@Database(version = MigrationDb.LATEST_VERSION,
- entities = {MigrationDb.Entity1.class, MigrationDb.Entity2.class,
- MigrationDb.Entity4.class})
-public abstract class MigrationDb extends RoomDatabase {
- static final int LATEST_VERSION = 7;
- abstract MigrationDao dao();
- @Entity(indices = {@Index(value = "name", unique = true)})
- static class Entity1 {
- public static final String TABLE_NAME = "Entity1";
- @PrimaryKey
- public int id;
- public String name;
- }
-
- @Entity
- static class Entity2 {
- public static final String TABLE_NAME = "Entity2";
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String addedInV3;
- public String name;
- }
-
- @Entity
- static class Entity3 { // added in version 4, removed at 6
- public static final String TABLE_NAME = "Entity3";
- @PrimaryKey
- public int id;
- @Ignore //removed at 5
- public String removedInV5;
- public String name;
- }
-
- @Entity(foreignKeys = {
- @ForeignKey(entity = Entity1.class,
- parentColumns = "name",
- childColumns = "name",
- deferred = true)})
- static class Entity4 {
- public static final String TABLE_NAME = "Entity4";
- @PrimaryKey
- public int id;
- @ColumnInfo(collate = ColumnInfo.NOCASE)
- public String name;
- }
-
- @Dao
- interface MigrationDao {
- @Query("SELECT * from Entity1 ORDER BY id ASC")
- List<Entity1> loadAllEntity1s();
- @Query("SELECT * from Entity2 ORDER BY id ASC")
- List<Entity2> loadAllEntity2s();
- @Query("SELECT * from Entity2 ORDER BY id ASC")
- List<Entity2Pojo> loadAllEntity2sAsPojo();
- @Insert
- void insert(Entity2... entity2);
- }
-
- static class Entity2Pojo extends Entity2 {
- }
-
- /**
- * not a real dao because database will change.
- */
- static class Dao_V1 {
- final SupportSQLiteDatabase mDb;
-
- Dao_V1(SupportSQLiteDatabase db) {
- mDb = db;
- }
-
- public void insertIntoEntity1(int id, String name) {
- ContentValues values = new ContentValues();
- values.put("id", id);
- values.put("name", name);
- long insertionId = mDb.insert(Entity1.TABLE_NAME,
- SQLiteDatabase.CONFLICT_REPLACE, values);
- if (insertionId == -1) {
- throw new RuntimeException("test sanity failure");
- }
- }
- }
-
- /**
- * not a real dao because database will change.
- */
- static class Dao_V2 {
- final SupportSQLiteDatabase mDb;
-
- Dao_V2(SupportSQLiteDatabase db) {
- mDb = db;
- }
-
- public void insertIntoEntity2(int id, String name) {
- ContentValues values = new ContentValues();
- values.put("id", id);
- values.put("name", name);
- long insertionId = mDb.insert(Entity2.TABLE_NAME,
- SQLiteDatabase.CONFLICT_REPLACE, values);
- if (insertionId == -1) {
- throw new RuntimeException("test sanity failure");
- }
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
deleted file mode 100644
index c850a4d..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ /dev/null
@@ -1,433 +0,0 @@
-/*
- * 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.migration;
-
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.endsWith;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.CoreMatchers.startsWith;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.migration.Migration;
-import android.arch.persistence.room.testing.MigrationTestHelper;
-import android.arch.persistence.room.util.TableInfo;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.hamcrest.MatcherAssert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Test custom database migrations.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MigrationTest {
- private static final String TEST_DB = "migration-test";
- @Rule
- public MigrationTestHelper helper;
-
- public MigrationTest() {
- helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
- MigrationDb.class.getCanonicalName());
- }
-
- @Test
- public void giveBadResource() throws IOException {
- MigrationTestHelper helper = new MigrationTestHelper(
- InstrumentationRegistry.getInstrumentation(),
- "foo", new FrameworkSQLiteOpenHelperFactory());
- try {
- helper.createDatabase(TEST_DB, 1);
- throw new AssertionError("must have failed with missing file exception");
- } catch (FileNotFoundException exception) {
- assertThat(exception.getMessage(), containsString("Cannot find"));
- }
- }
-
- @Test
- public void startInCurrentVersion() throws IOException {
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB,
- MigrationDb.LATEST_VERSION);
- final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
- dao.insertIntoEntity1(2, "x");
- db.close();
- MigrationDb migrationDb = getLatestDb();
- List<MigrationDb.Entity1> items = migrationDb.dao().loadAllEntity1s();
- helper.closeWhenFinished(migrationDb);
- assertThat(items.size(), is(1));
- }
-
- @Test
- public void addTable() throws IOException {
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
- final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
- dao.insertIntoEntity1(2, "foo");
- dao.insertIntoEntity1(3, "bar");
- db.close();
- db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
- new MigrationDb.Dao_V2(db).insertIntoEntity2(3, "blah");
- db.close();
- MigrationDb migrationDb = getLatestDb();
- List<MigrationDb.Entity1> entity1s = migrationDb.dao().loadAllEntity1s();
-
- assertThat(entity1s.size(), is(2));
- MigrationDb.Entity2 entity2 = new MigrationDb.Entity2();
- entity2.id = 2;
- entity2.name = "bar";
- // assert no error happens
- migrationDb.dao().insert(entity2);
- List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s();
- assertThat(entity2s.size(), is(2));
- }
-
- private MigrationDb getLatestDb() {
- MigrationDb db = Room.databaseBuilder(
- InstrumentationRegistry.getInstrumentation().getTargetContext(),
- MigrationDb.class, TEST_DB).addMigrations(ALL_MIGRATIONS).build();
- // trigger open
- db.beginTransaction();
- db.endTransaction();
- helper.closeWhenFinished(db);
- return db;
- }
-
- @Test
- public void addTableFailure() throws IOException {
- testFailure(1, 2);
- }
-
- @Test
- public void addColumnFailure() throws IOException {
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
- db.close();
- IllegalStateException caught = null;
- try {
- helper.runMigrationsAndValidate(TEST_DB, 3, true, new EmptyMigration(2, 3));
- } catch (IllegalStateException ex) {
- caught = ex;
- }
- assertThat(caught, instanceOf(IllegalStateException.class));
- }
-
- @Test
- public void addColumn() throws IOException {
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
- MigrationDb.Dao_V2 v2Dao = new MigrationDb.Dao_V2(db);
- v2Dao.insertIntoEntity2(7, "blah");
- db.close();
- helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3);
- // trigger open.
- MigrationDb migrationDb = getLatestDb();
- List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s();
- assertThat(entity2s.size(), is(1));
- assertThat(entity2s.get(0).name, is("blah"));
- assertThat(entity2s.get(0).addedInV3, is(nullValue()));
-
- List<MigrationDb.Entity2Pojo> entity2Pojos = migrationDb.dao().loadAllEntity2sAsPojo();
- assertThat(entity2Pojos.size(), is(1));
- assertThat(entity2Pojos.get(0).name, is("blah"));
- assertThat(entity2Pojos.get(0).addedInV3, is(nullValue()));
- }
-
- @Test
- public void failedToRemoveColumn() throws IOException {
- testFailure(4, 5);
- }
-
- @Test
- public void removeColumn() throws IOException {
- helper.createDatabase(TEST_DB, 4);
- final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
- 5, true, MIGRATION_4_5);
- final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
- assertThat(info.columns.size(), is(2));
- }
-
- @Test
- public void dropTable() throws IOException {
- helper.createDatabase(TEST_DB, 5);
- final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
- 6, true, MIGRATION_5_6);
- final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
- assertThat(info.columns.size(), is(0));
- }
-
- @Test
- public void failedToDropTable() throws IOException {
- testFailure(5, 6);
- }
-
- @Test
- public void failedToDropTableDontVerify() throws IOException {
- helper.createDatabase(TEST_DB, 5);
- final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
- 6, false, new EmptyMigration(5, 6));
- final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
- assertThat(info.columns.size(), is(2));
- }
-
- @Test
- public void failedForeignKey() throws IOException {
- final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 6);
- db.close();
- Throwable throwable = null;
- try {
- helper.runMigrationsAndValidate(TEST_DB,
- 7, false, new Migration(6, 7) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("CREATE TABLE Entity4 (`id` INTEGER NOT NULL,"
- + " `name` TEXT, PRIMARY KEY(`id`))");
- }
- });
- } catch (Throwable t) {
- throwable = t;
- }
- assertThat(throwable, instanceOf(IllegalStateException.class));
- //noinspection ConstantConditions
- assertThat(throwable.getMessage(), containsString("Migration failed"));
- }
-
- @Test
- public void newTableWithForeignKey() throws IOException {
- helper.createDatabase(TEST_DB, 6);
- final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
- 7, false, MIGRATION_6_7);
- final TableInfo info = TableInfo.read(db, MigrationDb.Entity4.TABLE_NAME);
- assertThat(info.foreignKeys.size(), is(1));
- }
-
- @Test
- public void missingMigration() throws IOException {
- SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
- database.close();
- try {
- Context targetContext = InstrumentationRegistry.getTargetContext();
- MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
- .build();
- db.dao().loadAllEntity1s();
- throw new AssertionError("Should've failed :/");
- } catch (IllegalStateException ignored) {
- }
- }
-
- @Test
- public void missingMigrationNuke() throws IOException {
- SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
- final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
- dao.insertIntoEntity1(2, "foo");
- dao.insertIntoEntity1(3, "bar");
- database.close();
-
- Context targetContext = InstrumentationRegistry.getTargetContext();
- MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
- .fallbackToDestructiveMigration()
- .build();
- assertThat(db.dao().loadAllEntity1s().size(), is(0));
- db.close();
- }
-
- @Test
- public void failWithIdentityCheck() throws IOException {
- for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) {
- String name = "test_" + i;
- helper.createDatabase(name, i).close();
- IllegalStateException exception = null;
- try {
- MigrationDb db = Room.databaseBuilder(
- InstrumentationRegistry.getInstrumentation().getTargetContext(),
- MigrationDb.class, name).build();
- db.runInTransaction(new Runnable() {
- @Override
- public void run() {
- // do nothing
- }
- });
- } catch (IllegalStateException ex) {
- exception = ex;
- }
- MatcherAssert.assertThat("identity detection should've failed",
- exception, notNullValue());
- }
- }
-
- @Test
- public void fallbackToDestructiveMigrationFrom_destructiveMigrationOccursForSuppliedVersion()
- throws IOException {
- SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
- final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
- dao.insertIntoEntity1(2, "foo");
- dao.insertIntoEntity1(3, "bar");
- database.close();
- Context targetContext = InstrumentationRegistry.getTargetContext();
-
- MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
- .fallbackToDestructiveMigrationFrom(6)
- .build();
-
- assertThat(db.dao().loadAllEntity1s().size(), is(0));
- }
-
- @Test
- public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationStartVersion_exception()
- throws IOException {
- SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
- database.close();
- Context targetContext = InstrumentationRegistry.getTargetContext();
-
- Throwable throwable = null;
- try {
- Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
- .addMigrations(MIGRATION_6_7)
- .fallbackToDestructiveMigrationFrom(6)
- .build();
- } catch (Throwable t) {
- throwable = t;
- }
-
- assertThat(throwable, is(not(nullValue())));
- //noinspection ConstantConditions
- assertThat(throwable.getMessage(),
- startsWith("Inconsistency detected. A Migration was supplied to"));
- assertThat(throwable.getMessage(), endsWith("6"));
- }
-
- @Test
- public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationEndVersion_exception()
- throws IOException {
- SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 5);
- database.close();
- Context targetContext = InstrumentationRegistry.getTargetContext();
-
- Throwable throwable = null;
- try {
- Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
- .addMigrations(MIGRATION_5_6)
- .fallbackToDestructiveMigrationFrom(6)
- .build();
- } catch (Throwable t) {
- throwable = t;
- }
-
- assertThat(throwable, is(not(nullValue())));
- //noinspection ConstantConditions
- assertThat(throwable.getMessage(),
- startsWith("Inconsistency detected. A Migration was supplied to"));
- assertThat(throwable.getMessage(), endsWith("6"));
- }
-
- private void testFailure(int startVersion, int endVersion) throws IOException {
- final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
- db.close();
- Throwable throwable = null;
- try {
- helper.runMigrationsAndValidate(TEST_DB, endVersion, true,
- new EmptyMigration(startVersion, endVersion));
- } catch (Throwable t) {
- throwable = t;
- }
- assertThat(throwable, instanceOf(IllegalStateException.class));
- //noinspection ConstantConditions
- assertThat(throwable.getMessage(), containsString("Migration failed"));
- }
-
- private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` ("
- + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
- + " `name` TEXT)");
- }
- };
-
- private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("ALTER TABLE " + MigrationDb.Entity2.TABLE_NAME
- + " ADD COLUMN addedInV3 TEXT");
- }
- };
-
- private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER NOT NULL,"
- + " `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))");
- }
- };
-
- private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER NOT NULL,"
- + " `name` TEXT, PRIMARY KEY(`id`))");
- database.execSQL("INSERT INTO Entity3_New(`id`, `name`) "
- + "SELECT `id`, `name` FROM Entity3");
- database.execSQL("DROP TABLE Entity3");
- database.execSQL("ALTER TABLE Entity3_New RENAME TO Entity3");
- }
- };
-
- private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("DROP TABLE " + MigrationDb.Entity3.TABLE_NAME);
- }
- };
-
- private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS " + MigrationDb.Entity4.TABLE_NAME
- + " (`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`)");
- }
- };
-
- private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_1_2,
- MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7};
-
- static final class EmptyMigration extends Migration {
- EmptyMigration(int startVersion, int endVersion) {
- super(startVersion, endVersion);
- }
-
- @Override
- public void migrate(SupportSQLiteDatabase database) {
- // do nothing
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
deleted file mode 100644
index c5af5c0..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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.paging;
-
-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 static org.junit.Assert.assertNotNull;
-
-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.DataSource;
-import android.arch.paging.LivePagedListBuilder;
-import android.arch.paging.PagedList;
-import android.arch.persistence.db.SimpleSQLiteQuery;
-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.Pet;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
-import android.support.annotation.Nullable;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class DataSourceFactoryTest extends TestDatabaseTest {
- @Rule
- public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
-
- private interface LivePagedListFactory {
- LiveData<PagedList<User>> create();
- }
-
- @Test
- public void getUsersAsPagedList()
- throws InterruptedException, ExecutionException, TimeoutException {
- validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
- mUserDao.loadPagedByAge(3),
- new PagedList.Config.Builder()
- .setPageSize(10)
- .setPrefetchDistance(1)
- .setInitialLoadSizeHint(10).build())
- .build());
- }
-
- @Test
- public void getUsersAsPagedList_ViaRawQuery_WithObservable()
- throws InterruptedException, ExecutionException, TimeoutException {
- SimpleSQLiteQuery query = new SimpleSQLiteQuery(
- "SELECT * FROM user where mAge > ?",
- new Object[]{3});
- validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
- mUserDao.loadPagedByAgeWithObserver(query),
- new PagedList.Config.Builder()
- .setPageSize(10)
- .setPrefetchDistance(1)
- .setInitialLoadSizeHint(10).build())
- .build());
- }
-
- private void validateUsersAsPagedList(
- LivePagedListFactory factory)
- throws InterruptedException, ExecutionException, TimeoutException {
- mDatabase.beginTransaction();
- try {
- for (int i = 0; i < 100; i++) {
- final User user = TestUtil.createUser(i + 1);
- user.setAge(i);
- mUserDao.insert(user);
- }
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- assertThat(mUserDao.count(), is(100));
-
- final LiveData<PagedList<User>> livePagedUsers = factory.create();
-
- final TestLifecycleOwner testOwner = new TestLifecycleOwner();
- testOwner.handleEvent(Lifecycle.Event.ON_CREATE);
- drain();
- PagedListObserver<User> observer = new PagedListObserver<>();
-
- observe(livePagedUsers, testOwner, observer);
- assertThat(observer.get(), nullValue());
- observer.reset();
-
- testOwner.handleEvent(Lifecycle.Event.ON_START);
- drain();
-
- PagedList<User> pagedList1 = observer.get();
- assertThat(pagedList1, is(notNullValue()));
-
- assertThat(pagedList1.size(), is(96));
- assertThat(getAndLoad(pagedList1, 20), is(nullValue()));
- drain();
- assertThat(getAndLoad(pagedList1, 31), nullValue());
- assertThat(getAndLoad(pagedList1, 20), notNullValue());
- assertThat(getAndLoad(pagedList1, 16), notNullValue());
-
- drain();
- assertThat(getAndLoad(pagedList1, 31), notNullValue());
- assertThat(getAndLoad(pagedList1, 50), nullValue());
- drain();
- assertThat(getAndLoad(pagedList1, 50), notNullValue());
- observer.reset();
- // now invalidate the database but don't get the new paged list
- mUserDao.updateById(50, "foo");
- assertThat(getAndLoad(pagedList1, 70), nullValue());
- drain();
- assertThat(getAndLoad(pagedList1, 70), nullValue());
- PagedList<User> pagedList = observer.get();
- assertThat(getAndLoad(pagedList, 70), notNullValue());
- }
-
- private <T> T getAndLoad(PagedList<T> list, int pos) {
- T result = list.get(pos);
- list.loadAround(pos);
- return result;
- }
-
- private void drain() throws InterruptedException, TimeoutException {
- mExecutorRule.drainTasks(60, TimeUnit.SECONDS);
- }
-
- private void observe(final LiveData liveData, final LifecycleOwner provider,
- final Observer observer) throws ExecutionException, InterruptedException {
- FutureTask<Void> futureTask = new FutureTask<>(() -> {
- //noinspection unchecked
- liveData.observe(provider, observer);
- return null;
- });
- ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
- futureTask.get();
- }
-
- @Test
- public void withRelation() throws ExecutionException, InterruptedException, TimeoutException {
- // verify DataSourceFactory can be created from a multi table join
- DataSource.Factory<Integer, UserAndAllPets> factory =
- mUserPetDao.dataSourceFactoryMultiTable();
- LiveData<PagedList<UserAndAllPets>> liveData =
- new LivePagedListBuilder<>(mUserPetDao.dataSourceFactoryMultiTable(), 10).build();
- assertNotNull(factory.create());
-
- PagedListObserver<UserAndAllPets> observer = new PagedListObserver<>();
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- observe(liveData, lifecycleOwner, observer);
- drain();
- assertThat(observer.get(), is(Collections.emptyList()));
-
- observer.reset();
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- drain();
- final UserAndAllPets noPets = observer.get().get(0);
- assertThat(noPets.user, is(user));
-
- observer.reset();
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
- mPetDao.insertAll(pets);
-
- drain();
- final UserAndAllPets withPets = observer.get().get(0);
- assertThat(withPets.user, is(user));
- assertThat(withPets.pets, is(Arrays.asList(pets)));
- }
-
- static class TestLifecycleOwner implements LifecycleOwner {
-
- private LifecycleRegistry mLifecycle;
-
- TestLifecycleOwner() {
- mLifecycle = new LifecycleRegistry(this);
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mLifecycle;
- }
-
- void handleEvent(Lifecycle.Event event) {
- mLifecycle.handleLifecycleEvent(event);
- }
- }
-
- private static class PagedListObserver<T> implements Observer<PagedList<T>> {
- private PagedList<T> mList;
-
- void reset() {
- mList = null;
- }
-
- public PagedList<T> get() {
- return mList;
- }
-
- @Override
- public void onChanged(@Nullable PagedList<T> pagedList) {
- mList = pagedList;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
deleted file mode 100644
index 89359be..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.paging;
-
-import static junit.framework.Assert.assertFalse;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-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.persistence.room.paging.LimitOffsetDataSource;
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LimitOffsetDataSourceTest extends TestDatabaseTest {
-
- @After
- public void teardown() {
- mUserDao.deleteEverything();
- }
-
- private LimitOffsetDataSource<User> loadUsersByAgeDesc() {
- return (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc().create();
- }
-
- @Test
- public void emptyPage() {
- LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
- assertThat(dataSource.countItems(), is(0));
- }
-
- @Test
- public void loadCount() {
- createUsers(6);
- LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
- assertThat(dataSource.countItems(), is(6));
- }
-
- @Test
- public void singleItem() {
- List<User> users = createUsers(1);
- LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
- assertThat(dataSource.countItems(), is(1));
- List<User> initial = dataSource.loadRange(0, 10);
-
- assertThat(initial.get(0), is(users.get(0)));
- assertFalse(dataSource.loadRange(1, 10).iterator().hasNext());
- }
-
- @Test
- public void initial() {
- List<User> users = createUsers(10);
- LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
- assertThat(dataSource.countItems(), is(10));
- List<User> initial = dataSource.loadRange(0, 1);
- assertThat(initial.get(0), is(users.get(0)));
- List<User> second = dataSource.loadRange(1, 1);
- assertThat(second.get(0), is(users.get(1)));
- }
-
- @Test
- public void loadAll() {
- List<User> users = createUsers(10);
-
- LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
- List<User> all = dataSource.loadRange(0, 10);
- assertThat(users, is(all));
- }
-
- @Test
- public void loadAfter() {
- List<User> users = createUsers(10);
- LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
- List<User> result = dataSource.loadRange(4, 2);
- assertThat(result, is(users.subList(4, 6)));
- }
-
- @NonNull
- private List<User> createUsers(int count) {
- List<User> users = new ArrayList<>();
- for (int i = 0; i < count; i++) {
- User user = TestUtil.createUser(i);
- user.setAge(1);
- mUserDao.insert(user);
- users.add(user);
- }
- return users;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java
deleted file mode 100644
index 70b33c4..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ClearAllTablesTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright 2018 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.hasItem;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
-import static org.junit.Assert.assertThat;
-
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.ForeignKey;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.InvalidationTracker;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-
-@SuppressWarnings("WeakerAccess")
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class ClearAllTablesTest {
-
- @Database(version = 1, entities = {Parent.class, Child.class}, exportSchema = false)
- public abstract static class ClearAllTablesDatabase extends RoomDatabase {
- abstract ClearAllTablesDao dao();
- }
-
- @Entity
- public static class Parent {
- @PrimaryKey
- public long id;
- public String name;
-
- public Parent(long id, String name) {
- this.id = id;
- this.name = name;
- }
- }
-
- @Entity(foreignKeys = {
- @ForeignKey(entity = Parent.class, parentColumns = "id", childColumns = "parentId")})
- public static class Child {
- @PrimaryKey
- public long id;
- public String name;
- public long parentId;
-
- public Child(long id, String name, long parentId) {
- this.id = id;
- this.name = name;
- this.parentId = parentId;
- }
- }
-
- @Dao
- public interface ClearAllTablesDao {
- @Insert
- void insertParent(Parent parent);
-
- @Insert
- void insertChild(Child child);
-
- @Query("SELECT COUNT(*) FROM Parent")
- int countParent();
-
- @Query("SELECT COUNT(*) FROM Child")
- int countChild();
- }
-
- private ClearAllTablesDatabase mDatabase;
- private ClearAllTablesDao mDao;
-
- @Before
- public void setUp() {
- final Context context = InstrumentationRegistry.getTargetContext();
- mDatabase = Room.inMemoryDatabaseBuilder(context, ClearAllTablesDatabase.class).build();
- mDao = mDatabase.dao();
- }
-
- @After
- public void closeDatabase() {
- mDatabase.close();
- }
-
- @Test
- public void simple() {
- mDao.insertParent(new Parent(1, "A"));
- assertThat(mDao.countParent(), is(1));
- mDatabase.clearAllTables();
- assertThat(mDao.countParent(), is(0));
- }
-
- @Test
- public void foreignKey() {
- mDao.insertParent(new Parent(1, "A"));
- mDao.insertChild(new Child(1, "a", 1));
- assertThat(mDao.countParent(), is(1));
- assertThat(mDao.countChild(), is(1));
- mDatabase.clearAllTables();
- assertThat(mDao.countParent(), is(0));
- assertThat(mDao.countChild(), is(0));
- }
-
- @Test
- public void observer() throws InterruptedException {
- mDao.insertParent(new Parent(1, "A"));
- assertThat(mDao.countParent(), is(1));
- final CountDownLatch latch = new CountDownLatch(1);
- mDatabase.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("Parent") {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- assertThat(tables, hasSize(1));
- assertThat(tables, hasItem("Parent"));
- assertThat(mDao.countParent(), is(0));
- latch.countDown();
- }
- });
- mDatabase.clearAllTables();
- assertThat(latch.await(3000, TimeUnit.MILLISECONDS), is(true));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CollationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CollationTest.java
deleted file mode 100644
index f81cb70..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CollationTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright 2018 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.MatcherAssert.assertThat;
-
-import android.arch.persistence.room.ColumnInfo;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.hamcrest.CoreMatchers;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class CollationTest {
- private CollateDb mDb;
- private CollateDao mDao;
- private Locale mDefaultLocale;
- private final CollateEntity mItem1 = new CollateEntity(1, "abı");
- private final CollateEntity mItem2 = new CollateEntity(2, "abi");
- private final CollateEntity mItem3 = new CollateEntity(3, "abj");
- private final CollateEntity mItem4 = new CollateEntity(4, "abç");
-
- @Before
- public void init() {
- mDefaultLocale = Locale.getDefault();
- }
-
- private void initDao(Locale systemLocale) {
- Locale.setDefault(systemLocale);
- mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
- CollateDb.class).build();
- mDao = mDb.dao();
- mDao.insert(mItem1);
- mDao.insert(mItem2);
- mDao.insert(mItem3);
- mDao.insert(mItem4);
- }
-
- @After
- public void closeDb() {
- mDb.close();
- Locale.setDefault(mDefaultLocale);
- }
-
- @Test
- public void localized() {
- initDao(new Locale("tr", "TR"));
- List<CollateEntity> result = mDao.sortedByLocalized();
- assertThat(result, CoreMatchers.is(Arrays.asList(
- mItem4, mItem1, mItem2, mItem3
- )));
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 21)
- public void localized_asUnicode() {
- initDao(Locale.getDefault());
- List<CollateEntity> result = mDao.sortedByLocalizedAsUnicode();
- assertThat(result, CoreMatchers.is(Arrays.asList(
- mItem4, mItem2, mItem1, mItem3
- )));
- }
-
- @Test
- public void unicode_asLocalized() {
- initDao(new Locale("tr", "TR"));
- List<CollateEntity> result = mDao.sortedByUnicodeAsLocalized();
- assertThat(result, CoreMatchers.is(Arrays.asList(
- mItem4, mItem1, mItem2, mItem3
- )));
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 21)
- public void unicode() {
- initDao(Locale.getDefault());
- List<CollateEntity> result = mDao.sortedByUnicode();
- assertThat(result, CoreMatchers.is(Arrays.asList(
- mItem4, mItem2, mItem1, mItem3
- )));
- }
-
- @SuppressWarnings("WeakerAccess")
- @android.arch.persistence.room.Entity
- static class CollateEntity {
- @PrimaryKey
- public final int id;
- @ColumnInfo(collate = ColumnInfo.LOCALIZED)
- public final String localizedName;
- @ColumnInfo(collate = ColumnInfo.UNICODE)
- public final String unicodeName;
-
- CollateEntity(int id, String name) {
- this.id = id;
- this.localizedName = name;
- this.unicodeName = name;
- }
-
- CollateEntity(int id, String localizedName, String unicodeName) {
- this.id = id;
- this.localizedName = localizedName;
- this.unicodeName = unicodeName;
- }
-
- @SuppressWarnings("SimplifiableIfStatement")
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- CollateEntity that = (CollateEntity) o;
-
- if (id != that.id) return false;
- if (!localizedName.equals(that.localizedName)) return false;
- return unicodeName.equals(that.unicodeName);
- }
-
- @Override
- public int hashCode() {
- int result = id;
- result = 31 * result + localizedName.hashCode();
- result = 31 * result + unicodeName.hashCode();
- return result;
- }
-
- @Override
- public String toString() {
- return "CollateEntity{"
- + "id=" + id
- + ", localizedName='" + localizedName + '\''
- + ", unicodeName='" + unicodeName + '\''
- + '}';
- }
- }
-
- @Dao
- interface CollateDao {
- @Query("SELECT * FROM CollateEntity ORDER BY localizedName ASC")
- List<CollateEntity> sortedByLocalized();
-
- @Query("SELECT * FROM CollateEntity ORDER BY localizedName COLLATE UNICODE ASC")
- List<CollateEntity> sortedByLocalizedAsUnicode();
-
- @Query("SELECT * FROM CollateEntity ORDER BY unicodeName ASC")
- List<CollateEntity> sortedByUnicode();
-
- @Query("SELECT * FROM CollateEntity ORDER BY unicodeName COLLATE LOCALIZED ASC")
- List<CollateEntity> sortedByUnicodeAsLocalized();
-
- @Insert
- void insert(CollateEntity... entities);
- }
-
- @Database(entities = CollateEntity.class, version = 1, exportSchema = false)
- abstract static class CollateDb extends RoomDatabase {
- abstract CollateDao dao();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
deleted file mode 100644
index 46c875c..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Embedded;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-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;
-
-@RunWith(AndroidJUnit4.class)
-@SuppressWarnings("SqlNoDataSourceInspection")
-@SmallTest
-public class ConstructorTest {
- @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class,
- EntityWithAnnotations.class},
- exportSchema = false)
- abstract static class MyDb extends RoomDatabase {
- abstract MyDao dao();
- }
-
- @Dao
- interface MyDao {
- @Insert
- void insertFull(FullConstructor... full);
-
- @Query("SELECT * FROM fc WHERE a = :a")
- FullConstructor loadFull(int a);
-
- @Insert
- void insertPartial(PartialConstructor... partial);
-
- @Query("SELECT * FROM pc WHERE a = :a")
- PartialConstructor loadPartial(int a);
-
- @Insert
- void insertEntityWithAnnotations(EntityWithAnnotations... items);
-
- @Query("SELECT * FROM EntityWithAnnotations")
- EntityWithAnnotations getEntitiWithAnnotations();
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity(tableName = "fc")
- static class FullConstructor {
- @PrimaryKey
- public final int a;
- public final int b;
- @Embedded
- public final MyEmbedded embedded;
-
- FullConstructor(int a, int b, MyEmbedded embedded) {
- this.a = a;
- this.b = b;
- this.embedded = embedded;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- FullConstructor that = (FullConstructor) o;
-
- if (a != that.a) return false;
- //noinspection SimplifiableIfStatement
- if (b != that.b) return false;
- return embedded != null ? embedded.equals(that.embedded)
- : that.embedded == null;
- }
-
- @Override
- public int hashCode() {
- int result = a;
- result = 31 * result + b;
- result = 31 * result + (embedded != null ? embedded.hashCode() : 0);
- return result;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity(tableName = "pc")
- static class PartialConstructor {
- @PrimaryKey
- public final int a;
- public int b;
- @Embedded
- private MyEmbedded mEmbedded;
-
- PartialConstructor(int a) {
- this.a = a;
- }
-
- public MyEmbedded getEmbedded() {
- return mEmbedded;
- }
-
- public void setEmbedded(MyEmbedded embedded) {
- mEmbedded = embedded;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- PartialConstructor that = (PartialConstructor) o;
-
- if (a != that.a) return false;
- //noinspection SimplifiableIfStatement
- if (b != that.b) return false;
- return mEmbedded != null ? mEmbedded.equals(that.mEmbedded)
- : that.mEmbedded == null;
- }
-
- @Override
- public int hashCode() {
- int result = a;
- result = 31 * result + b;
- result = 31 * result + (mEmbedded != null ? mEmbedded.hashCode() : 0);
- return result;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- static class MyEmbedded {
- public final String text;
-
- MyEmbedded(String text) {
- this.text = text;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- MyEmbedded that = (MyEmbedded) o;
-
- return text != null ? text.equals(that.text) : that.text == null;
- }
-
- @Override
- public int hashCode() {
- return text != null ? text.hashCode() : 0;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity
- static class EntityWithAnnotations {
- @PrimaryKey
- @NonNull
- public final String id;
-
- @NonNull
- public final String username;
-
- @Nullable
- public final String displayName;
-
- EntityWithAnnotations(
- @NonNull String id,
- @NonNull String username,
- @Nullable String displayName) {
- this.id = id;
- this.username = username;
- this.displayName = displayName;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- EntityWithAnnotations that = (EntityWithAnnotations) o;
-
- if (!id.equals(that.id)) return false;
- if (!username.equals(that.username)) return false;
- return displayName != null ? displayName.equals(that.displayName)
- : that.displayName == null;
- }
-
- @Override
- public int hashCode() {
- int result = id.hashCode();
- result = 31 * result + username.hashCode();
- result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
- return result;
- }
- }
-
- private MyDb mDb;
- private MyDao mDao;
-
- @Before
- public void init() {
- mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), MyDb.class)
- .build();
- mDao = mDb.dao();
- }
-
- @Test
- public void insertAndReadFullConstructor() {
- FullConstructor inserted = new FullConstructor(1, 2, null);
- mDao.insertFull(inserted);
- final FullConstructor load = mDao.loadFull(1);
- assertThat(load, is(inserted));
- }
-
- @Test
- public void insertAndReadPartial() {
- PartialConstructor item = new PartialConstructor(3);
- item.b = 7;
- mDao.insertPartial(item);
- PartialConstructor load = mDao.loadPartial(3);
- assertThat(load, is(item));
- }
-
- @Test // for bug b/69562125
- public void entityWithAnnotations() {
- EntityWithAnnotations item = new EntityWithAnnotations("a", "b", null);
- mDao.insertEntityWithAnnotations(item);
- assertThat(mDao.getEntitiWithAnnotations(), is(item));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
deleted file mode 100644
index 6f44546..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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.mockito.AdditionalAnswers.delegatesTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.integration.testapp.database.Customer;
-import android.arch.persistence.room.integration.testapp.database.SampleDatabase;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class CustomDatabaseTest {
-
- @Test
- public void invalidationTrackerAfterClose() {
- Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
- RoomDatabase.Builder<SampleDatabase> builder =
- Room.databaseBuilder(context, SampleDatabase.class, "db")
- .openHelperFactory(new RethrowExceptionFactory());
- Customer customer = new Customer();
- for (int i = 0; i < 100; i++) {
- SampleDatabase db = builder.build();
- db.getCustomerDao().insert(customer);
- // Give InvalidationTracker enough time to start #mRefreshRunnable and pass the
- // initialization check.
- SystemClock.sleep(1);
- // InvalidationTracker#mRefreshRunnable will cause race condition if its database query
- // happens after close.
- db.close();
- }
- }
-
- /**
- * This is mostly {@link FrameworkSQLiteOpenHelperFactory}, but the returned {@link
- * SupportSQLiteDatabase} fails with {@link RuntimeException} instead of {@link
- * IllegalStateException} or {@link SQLiteException}. This way, we can simulate custom database
- * implementation that throws its own exception types.
- */
- private static class RethrowExceptionFactory implements SupportSQLiteOpenHelper.Factory {
-
- @Override
- public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
- final FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory();
- final SupportSQLiteOpenHelper helper = factory.create(configuration);
- SupportSQLiteOpenHelper helperMock = mock(SupportSQLiteOpenHelper.class,
- delegatesTo(helper));
- // Inject mocks to the object hierarchy.
- doAnswer(new Answer() {
- @Override
- public SupportSQLiteDatabase answer(InvocationOnMock invocation)
- throws Throwable {
- final SupportSQLiteDatabase db = helper.getWritableDatabase();
- SupportSQLiteDatabase dbMock = mock(SupportSQLiteDatabase.class,
- delegatesTo(db));
- doAnswer(new Answer() {
- @Override
- public Cursor answer(InvocationOnMock invocation) throws Throwable {
- SupportSQLiteQuery query = invocation.getArgument(0);
- try {
- return db.query(query);
- } catch (IllegalStateException | SQLiteException e) {
- // Rethrow the exception in order to simulate the way custom
- // database implementation throws its own exception types.
- throw new RuntimeException("closed", e);
- }
- }
- }).when(dbMock).query(any(SupportSQLiteQuery.class));
- return dbMock;
- }
- }).when(helperMock).getWritableDatabase();
- return helperMock;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/DaoNameConflictTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/DaoNameConflictTest.java
deleted file mode 100644
index 07c0369..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/DaoNameConflictTest.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright 2018 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.MatcherAssert.assertThat;
-
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class DaoNameConflictTest {
- private ConflictDatabase mDb;
- @Before
- public void init() {
- mDb = Room.inMemoryDatabaseBuilder(
- InstrumentationRegistry.getTargetContext(),
- ConflictDatabase.class
- ).build();
- }
-
- @After
- public void close() {
- mDb.close();
- }
-
- @Test
- public void readFromItem1() {
- Item1 item1 = new Item1(1, "a");
- mDb.item1Dao().insert(item1);
- Item2 item2 = new Item2(2, "b");
- mDb.item2Dao().insert(item2);
- assertThat(mDb.item1Dao().get(), is(item1));
- assertThat(mDb.item2Dao().get(), is(item2));
- }
-
- @Entity
- static class Item1 {
- @PrimaryKey
- public int id;
- public String name;
-
- Item1(int id, String name) {
- this.id = id;
- this.name = name;
- }
-
- @Dao
- public interface Store {
- @Query("SELECT * FROM Item1 LIMIT 1")
- Item1 get();
- @Insert
- void insert(Item1... items);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Item1 item1 = (Item1) o;
-
- //noinspection SimplifiableIfStatement
- if (id != item1.id) return false;
- return name != null ? name.equals(item1.name) : item1.name == null;
- }
-
- @Override
- public int hashCode() {
- int result = id;
- result = 31 * result + (name != null ? name.hashCode() : 0);
- return result;
- }
- }
-
- @Entity
- static class Item2 {
- @PrimaryKey
- public int id;
- public String name;
-
- Item2(int id, String name) {
- this.id = id;
- this.name = name;
- }
-
- @Dao
- public interface Store {
- @Query("SELECT * FROM Item2 LIMIT 1")
- Item2 get();
- @Insert
- void insert(Item2... items);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Item2 item2 = (Item2) o;
-
- //noinspection SimplifiableIfStatement
- if (id != item2.id) return false;
- return name != null ? name.equals(item2.name) : item2.name == null;
- }
-
- @Override
- public int hashCode() {
- int result = id;
- result = 31 * result + (name != null ? name.hashCode() : 0);
- return result;
- }
- }
-
- @Database(version = 1, exportSchema = false, entities = {Item1.class, Item2.class})
- public abstract static class ConflictDatabase extends RoomDatabase {
- public abstract Item1.Store item1Dao();
- public abstract Item2.Store item2Dao();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
deleted file mode 100644
index fafcc2c..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.core.IsCollectionContaining.hasItem;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.content.Context;
-import android.database.Cursor;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-public class DatabaseCallbackTest {
-
- @Test
- @MediumTest
- public void createAndOpen() {
- Context context = InstrumentationRegistry.getTargetContext();
- TestDatabaseCallback callback1 = new TestDatabaseCallback();
- TestDatabase db1 = null;
- TestDatabase db2 = null;
- try {
- db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
- .addCallback(callback1)
- .build();
- assertFalse(callback1.mCreated);
- assertFalse(callback1.mOpened);
- User user1 = TestUtil.createUser(3);
- user1.setName("george");
- db1.getUserDao().insert(user1);
- assertTrue(callback1.mCreated);
- assertTrue(callback1.mOpened);
- TestDatabaseCallback callback2 = new TestDatabaseCallback();
- db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
- .addCallback(callback2)
- .build();
- assertFalse(callback2.mCreated);
- assertFalse(callback2.mOpened);
- User user2 = db2.getUserDao().load(3);
- assertThat(user2.getName(), is("george"));
- assertFalse(callback2.mCreated); // Not called; already created by db1
- assertTrue(callback2.mOpened);
- } finally {
- if (db1 != null) {
- db1.close();
- }
- if (db2 != null) {
- db2.close();
- }
- assertTrue(context.deleteDatabase("test"));
- }
- }
-
- @Test
- @SmallTest
- public void writeOnCreate() {
- Context context = InstrumentationRegistry.getTargetContext();
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .addCallback(new RoomDatabase.Callback() {
- @Override
- public void onCreate(@NonNull SupportSQLiteDatabase db) {
- Cursor cursor = null;
- try {
- cursor = db.query(
- "SELECT name FROM sqlite_master WHERE type = 'table'");
- ArrayList<String> names = new ArrayList<>();
- while (cursor.moveToNext()) {
- names.add(cursor.getString(0));
- }
- assertThat(names, hasItem("User"));
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
- })
- .build();
- List<Integer> ids = db.getUserDao().loadIds();
- assertThat(ids, is(empty()));
- }
-
- public static class TestDatabaseCallback extends RoomDatabase.Callback {
-
- boolean mCreated;
- boolean mOpened;
-
- @Override
- public void onCreate(@NonNull SupportSQLiteDatabase db) {
- mCreated = true;
- }
-
- @Override
- public void onOpen(@NonNull SupportSQLiteDatabase db) {
- mOpened = true;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/EmbeddedTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/EmbeddedTest.java
deleted file mode 100644
index d680f3d..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/EmbeddedTest.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.persistence.room.Room;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-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;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
-import android.arch.persistence.room.integration.testapp.vo.Coordinates;
-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.School;
-import android.arch.persistence.room.integration.testapp.vo.SchoolRef;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
-import android.arch.persistence.room.integration.testapp.vo.UserAndPetNonNull;
-import android.content.Context;
-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.ArrayList;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class EmbeddedTest {
- private UserDao mUserDao;
- private PetDao mPetDao;
- private UserPetDao mUserPetDao;
- private SchoolDao mSchoolDao;
- private PetCoupleDao mPetCoupleDao;
-
- @Before
- public void createDb() {
- Context context = InstrumentationRegistry.getTargetContext();
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
- mUserDao = db.getUserDao();
- mPetDao = db.getPetDao();
- mUserPetDao = db.getUserPetDao();
- mSchoolDao = db.getSchoolDao();
- mPetCoupleDao = db.getPetCoupleDao();
- }
-
- @Test
- public void loadAll() {
- Pet pet = TestUtil.createPet(1);
- User user = TestUtil.createUser(2);
- pet.setUserId(user.getId());
- mUserDao.insert(user);
- mPetDao.insertOrReplace(pet);
- List<UserAndPet> all = mUserPetDao.loadAll();
- assertThat(all.size(), is(1));
- assertThat(all.get(0).getUser(), is(user));
- assertThat(all.get(0).getPet(), is(pet));
- }
-
- @Test
- public void loadFromUsers() {
- Pet pet = TestUtil.createPet(1);
- User user = TestUtil.createUser(2);
- pet.setUserId(user.getId());
- mUserDao.insert(user);
- mPetDao.insertOrReplace(pet);
- List<UserAndPet> all = mUserPetDao.loadUsers();
- assertThat(all.size(), is(1));
- assertThat(all.get(0).getUser(), is(user));
- assertThat(all.get(0).getPet(), is(pet));
- }
-
- @Test
- public void loadFromUsersWithNullPet() {
- User user = TestUtil.createUser(2);
- mUserDao.insert(user);
- List<UserAndPet> all = mUserPetDao.loadUsers();
- assertThat(all.size(), is(1));
- assertThat(all.get(0).getUser(), is(user));
- assertThat(all.get(0).getPet(), is(nullValue()));
- }
-
- @Test
- public void loadFromUsersWithNonNullPet() {
- User user = TestUtil.createUser(2);
- mUserDao.insert(user);
- List<UserAndPetNonNull> all = mUserPetDao.loadUsersWithNonNullPet();
- assertThat(all.size(), is(1));
- assertThat(all.get(0).getUser(), is(user));
- assertThat(all.get(0).getPet(), is(new Pet()));
- }
-
- @Test
- public void loadFromPets() {
- Pet pet = TestUtil.createPet(1);
- User user = TestUtil.createUser(2);
- pet.setUserId(user.getId());
- mUserDao.insert(user);
- mPetDao.insertOrReplace(pet);
- List<UserAndPet> all = mUserPetDao.loadPets();
- assertThat(all.size(), is(1));
- assertThat(all.get(0).getUser(), is(user));
- assertThat(all.get(0).getPet(), is(pet));
- }
-
- @Test
- public void loadFromPetsWithNullUser() {
- Pet pet = TestUtil.createPet(1);
- mPetDao.insertOrReplace(pet);
- List<UserAndPet> all = mUserPetDao.loadPets();
- assertThat(all.size(), is(1));
- assertThat(all.get(0).getUser(), is(nullValue()));
- assertThat(all.get(0).getPet(), is(pet));
- }
-
- @Test
- public void findSchoolByStreet() {
- School school = TestUtil.createSchool(3, 5);
- school.getAddress().setStreet("foo");
- mSchoolDao.insert(school);
- List<School> result = mSchoolDao.findByStreet("foo");
- assertThat(result.size(), is(1));
- assertThat(result.get(0), is(school));
- }
-
- @Test
- public void loadSubFieldsAsPojo() throws Exception {
- loadSubFieldsTest(new Callable<List<School>>() {
- @Override
- public List<School> call() throws Exception {
- List<School> result = new ArrayList<>();
- for (SchoolRef ref : mSchoolDao.schoolAndManagerNamesAsPojo()) {
- result.add(ref);
- }
- return result;
- }
- });
- }
-
- @Test
- public void loadSubFieldsAsEntity() throws Exception {
- loadSubFieldsTest(new Callable<List<School>>() {
- @Override
- public List<School> call() throws Exception {
- return mSchoolDao.schoolAndManagerNames();
- }
- });
- }
-
- public void loadSubFieldsTest(Callable<List<School>> loader) throws Exception {
- School school = TestUtil.createSchool(3, 5);
- school.setName("MTV High");
- school.getManager().setName("chet");
- mSchoolDao.insert(school);
-
- School school2 = TestUtil.createSchool(4, 6);
- school2.setName("MTV Low");
- school2.setManager(null);
- mSchoolDao.insert(school2);
-
- List<School> schools = loader.call();
- assertThat(schools.size(), is(2));
- assertThat(schools.get(0).getName(), is("MTV High"));
- assertThat(schools.get(1).getName(), is("MTV Low"));
- assertThat(schools.get(0).address, nullValue());
- assertThat(schools.get(1).address, nullValue());
- assertThat(schools.get(0).getManager(), notNullValue());
- assertThat(schools.get(1).getManager(), nullValue());
- assertThat(schools.get(0).getManager().getName(), is("chet"));
- }
-
- @Test
- public void loadNestedSub() {
- School school = TestUtil.createSchool(3, 5);
- school.getAddress().getCoordinates().lat = 3.;
- school.getAddress().getCoordinates().lng = 4.;
- mSchoolDao.insert(school);
- Coordinates coordinates = mSchoolDao.loadCoordinates(3);
- assertThat(coordinates.lat, is(3.));
- assertThat(coordinates.lng, is(4.));
-
- School asSchool = mSchoolDao.loadCoordinatesAsSchool(3);
- assertThat(asSchool.address.getCoordinates().lat, is(3.));
- assertThat(asSchool.address.getCoordinates().lng, is(4.));
- // didn't as for it so don't load
- assertThat(asSchool.getManager(), nullValue());
- assertThat(asSchool.address.getStreet(), nullValue());
- }
-
- @Test
- public void sameFieldType() {
- Pet male = TestUtil.createPet(3);
- Pet female = TestUtil.createPet(5);
- PetCouple petCouple = new PetCouple();
- petCouple.id = "foo";
- petCouple.male = male;
- petCouple.setFemale(female);
- mPetCoupleDao.insert(petCouple);
- List<PetCouple> petCouples = mPetCoupleDao.loadAll();
- assertThat(petCouples.size(), is(1));
- PetCouple loaded = petCouples.get(0);
- assertThat(loaded.id, is("foo"));
- assertThat(loaded.male, is(male));
- assertThat(loaded.getFemale(), is(female));
- }
-
- @Test
- public void sameFieldOneNull() {
- Pet loneWolf = TestUtil.createPet(3);
- PetCouple petCouple = new PetCouple();
- petCouple.id = "foo";
- petCouple.male = loneWolf;
- mPetCoupleDao.insert(petCouple);
- List<PetCouple> petCouples = mPetCoupleDao.loadAll();
- assertThat(petCouples.size(), is(1));
- PetCouple loaded = petCouples.get(0);
- assertThat(loaded.id, is("foo"));
- assertThat(loaded.male, is(loneWolf));
- assertThat(loaded.getFemale(), is(nullValue()));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ForeignKeyTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ForeignKeyTest.java
deleted file mode 100644
index 1c94e18..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/ForeignKeyTest.java
+++ /dev/null
@@ -1,425 +0,0 @@
-/*
- * 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.both;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.either;
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Delete;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.ForeignKey;
-import android.arch.persistence.room.Ignore;
-import android.arch.persistence.room.Index;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.database.sqlite.SQLiteException;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.hamcrest.Matcher;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Locale;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class ForeignKeyTest {
- @Database(version = 1, entities = {A.class, B.class, C.class, D.class, E.class},
- exportSchema = false)
- abstract static class ForeignKeyDb extends RoomDatabase {
- abstract FkDao dao();
- }
-
- @SuppressWarnings({"SqlNoDataSourceInspection", "SameParameterValue"})
- @Dao
- interface FkDao {
- @Insert
- void insert(A... a);
-
- @Insert
- void insert(B... b);
-
- @Insert
- void insert(C... c);
-
- @Insert
- void insert(D... d);
-
- @Query("SELECT * FROM A WHERE id = :id")
- A loadA(int id);
-
- @Query("SELECT * FROM B WHERE id = :id")
- B loadB(int id);
-
- @Query("SELECT * FROM C WHERE id = :id")
- C loadC(int id);
-
- @Query("SELECT * FROM D WHERE id = :id")
- D loadD(int id);
-
- @Query("SELECT * FROM E WHERE id = :id")
- E loadE(int id);
-
- @Delete
- void delete(A... a);
-
- @Delete
- void delete(B... b);
-
- @Delete
- void delete(C... c);
-
- @Query("UPDATE A SET name = :newName WHERE id = :id")
- void changeNameA(int id, String newName);
-
- @Insert
- void insert(E... e);
-
-
- }
-
- @Entity(indices = {@Index(value = "name", unique = true),
- @Index(value = {"name", "lastName"}, unique = true)})
- static class A {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String name;
- public String lastName;
-
- A(String name) {
- this.name = name;
- }
-
- @Ignore
- A(String name, String lastName) {
- this.name = name;
- this.lastName = lastName;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity(foreignKeys = {
- @ForeignKey(entity = A.class,
- parentColumns = "name",
- childColumns = "aName")})
-
- static class B {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String aName;
-
- B(String aName) {
- this.aName = aName;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity(foreignKeys = {
- @ForeignKey(entity = A.class,
- parentColumns = "name",
- childColumns = "aName",
- deferred = true)})
- static class C {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String aName;
-
- C(String aName) {
- this.aName = aName;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity(foreignKeys = {
- @ForeignKey(entity = A.class,
- parentColumns = "name",
- childColumns = "aName",
- onDelete = ForeignKey.CASCADE,
- onUpdate = ForeignKey.CASCADE)})
- static class D {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String aName;
-
- D(String aName) {
- this.aName = aName;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity(foreignKeys = {
- @ForeignKey(entity = A.class,
- parentColumns = {"name", "lastName"},
- childColumns = {"aName", "aLastName"},
- onDelete = ForeignKey.SET_NULL,
- onUpdate = ForeignKey.CASCADE)})
- static class E {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String aName;
- public String aLastName;
-
- E() {
- }
-
- @Ignore
- E(String aName, String aLastName) {
- this.aName = aName;
- this.aLastName = aLastName;
- }
- }
-
-
- private ForeignKeyDb mDb;
- private FkDao mDao;
-
- @Before
- public void openDb() {
- mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
- ForeignKeyDb.class).build();
- mDao = mDb.dao();
- }
-
- @Test
- public void simpleForeignKeyFailure() {
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- mDao.insert(new B("foo"));
- }
- });
- assertThat(t, instanceOf(SQLiteException.class));
- assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
- }
-
- @Test
- public void simpleForeignKeyDeferredFailure() {
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- mDao.insert(new C("foo"));
- }
- });
- assertThat(t, instanceOf(SQLiteException.class));
- assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
- }
-
- @Test
- public void immediateForeignKeyFailure() {
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- try {
- mDb.beginTransaction();
- mDao.insert(new B("foo"));
- mDao.insert(new A("foo"));
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- }
- });
- assertThat(t, instanceOf(SQLiteException.class));
- }
-
- @Test
- public void deferredForeignKeySuccess() {
- try {
- mDb.beginTransaction();
- mDao.insert(new C("foo"));
- mDao.insert(new A("foo"));
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- assertThat(mDao.loadA(1), notNullValue());
- assertThat(mDao.loadC(1), notNullValue());
- }
-
- @Test
- public void onDelete_noAction() {
- mDao.insert(new A("a1"));
- final A a = mDao.loadA(1);
- mDao.insert(new B("a1"));
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- mDao.delete(a);
- }
- });
- assertThat(t, instanceOf(SQLiteException.class));
- assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
- }
-
- @Test
- public void onDelete_noAction_withTransaction() {
- mDao.insert(new A("a1"));
- final A a = mDao.loadA(1);
- mDao.insert(new B("a1"));
- final B b = mDao.loadB(1);
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- deleteInTransaction(a, b);
- }
- });
- assertThat(t, instanceOf(SQLiteException.class));
- assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
- }
-
- @Test
- public void onDelete_noAction_deferred() {
- mDao.insert(new A("a1"));
- final A a = mDao.loadA(1);
- mDao.insert(new C("a1"));
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- mDao.delete(a);
- }
- });
- assertThat(t, instanceOf(SQLiteException.class));
- assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
- }
-
- @Test
- public void onDelete_noAction__deferredWithTransaction() {
- mDao.insert(new A("a1"));
- final A a = mDao.loadA(1);
- mDao.insert(new C("a1"));
- final C c = mDao.loadC(1);
- deleteInTransaction(a, c);
- }
-
- @Test
- public void onDelete_cascade() {
- mDao.insert(new A("a1"));
- final A a = mDao.loadA(1);
- mDao.insert(new D("a1"));
- final D d = mDao.loadD(1);
- assertThat("test sanity", d, notNullValue());
- mDao.delete(a);
- assertThat(mDao.loadD(1), nullValue());
- }
-
- @Test
- public void onUpdate_cascade() {
- mDao.insert(new A("a1"));
- mDao.insert(new D("a1"));
- final D d = mDao.loadD(1);
- assertThat("test sanity", d, notNullValue());
- mDao.changeNameA(1, "bla");
- assertThat(mDao.loadD(1).aName, equalTo("bla"));
- assertThat(mDao.loadA(1).name, equalTo("bla"));
- }
-
- @Test
- public void multipleReferences() {
- mDao.insert(new A("a1", "a2"));
- final A a = mDao.loadA(1);
- assertThat("test sanity", a, notNullValue());
- Throwable t = catchException(new ThrowingRunnable() {
- @Override
- public void run() throws Exception {
- mDao.insert(new E("a1", "dsa"));
- }
- });
- assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
- }
-
- @Test
- public void onDelete_setNull_multipleReferences() {
- mDao.insert(new A("a1", "a2"));
- final A a = mDao.loadA(1);
- mDao.insert(new E("a1", "a2"));
- assertThat(mDao.loadE(1), notNullValue());
- mDao.delete(a);
- E e = mDao.loadE(1);
- assertThat(e, notNullValue());
- assertThat(e.aName, nullValue());
- assertThat(e.aLastName, nullValue());
- }
-
- @Test
- public void onUpdate_cascade_multipleReferences() {
- mDao.insert(new A("a1", "a2"));
- final A a = mDao.loadA(1);
- mDao.insert(new E("a1", "a2"));
- assertThat(mDao.loadE(1), notNullValue());
- mDao.changeNameA(1, "foo");
- assertThat(mDao.loadE(1), notNullValue());
- assertThat(mDao.loadE(1).aName, equalTo("foo"));
- assertThat(mDao.loadE(1).aLastName, equalTo("a2"));
- }
-
- private static Matcher<String> foreignKeyErrorMessage() {
- return either(containsString("FOREIGN KEY"))
- .or(both(containsString("CODE 19")).and(containsString("CONSTRAINT FAILED")));
- }
-
- @SuppressWarnings("Duplicates")
- private void deleteInTransaction(A a, B b) {
- mDb.beginTransaction();
- try {
- mDao.delete(a);
- mDao.delete(b);
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- }
-
- @SuppressWarnings("Duplicates")
- private void deleteInTransaction(A a, C c) {
- mDb.beginTransaction();
- try {
- mDao.delete(a);
- mDao.delete(c);
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- }
-
- private static Throwable catchException(ThrowingRunnable throwingRunnable) {
- try {
- throwingRunnable.run();
- } catch (Throwable t) {
- return t;
- }
- throw new RuntimeException("didn't throw an exception");
- }
-
- private interface ThrowingRunnable {
- void run() throws Exception;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java
deleted file mode 100644
index dcefa41..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.persistence.room.integration.testapp.vo.FunnyNamedEntity;
-import android.support.test.InstrumentationRegistry;
-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];
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> mFunnyNamedDao.observableOne(2).observeForever(
- 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/GenericEntityTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/GenericEntityTest.java
deleted file mode 100644
index 8efb7c7..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/GenericEntityTest.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright 2018 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.MatcherAssert.assertThat;
-
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class GenericEntityTest {
- private GenericDb mDb;
- private GenericDao mDao;
-
- @Before
- public void init() {
- mDb = Room.inMemoryDatabaseBuilder(
- InstrumentationRegistry.getTargetContext(),
- GenericDb.class
- ).build();
- mDao = mDb.getDao();
- }
-
- @After
- public void close() {
- mDb.close();
- }
-
- @Test
- public void readWriteEntity() {
- EntityItem item = new EntityItem("abc", "def");
- mDao.insert(item);
- EntityItem received = mDao.get("abc");
- assertThat(received, is(item));
- }
-
- @Test
- public void readPojo() {
- EntityItem item = new EntityItem("abc", "def");
- mDao.insert(item);
- PojoItem received = mDao.getPojo("abc");
- assertThat(received.id, is("abc"));
- }
-
- static class Item<P, F> {
- @NonNull
- @PrimaryKey
- public final P id;
- private F mField;
-
- Item(@NonNull P id) {
- this.id = id;
- }
-
- public F getField() {
- return mField;
- }
-
- public void setField(F field) {
- mField = field;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Item<?, ?> item = (Item<?, ?>) o;
- //noinspection SimplifiableIfStatement
- if (!id.equals(item.id)) return false;
- return mField != null ? mField.equals(item.mField) : item.mField == null;
- }
-
- @Override
- public int hashCode() {
- int result = id.hashCode();
- result = 31 * result + (mField != null ? mField.hashCode() : 0);
- return result;
- }
- }
-
- static class PojoItem extends Item<String, Integer> {
- PojoItem(String id) {
- super(id);
- }
- }
-
- @Entity
- static class EntityItem extends Item<String, Integer> {
- public final String name;
-
- EntityItem(String id, String name) {
- super(id);
- this.name = name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
- EntityItem that = (EntityItem) o;
- return name != null ? name.equals(that.name) : that.name == null;
- }
-
- @Override
- public int hashCode() {
- int result = super.hashCode();
- result = 31 * result + (name != null ? name.hashCode() : 0);
- return result;
- }
- }
-
- @Dao
- public interface GenericDao {
- @Insert
- void insert(EntityItem... items);
-
- @Query("SELECT * FROM EntityItem WHERE id = :id")
- EntityItem get(String id);
-
- @Query("SELECT * FROM EntityItem WHERE id = :id")
- PojoItem getPojo(String id);
- }
-
- @Database(version = 1, entities = {EntityItem.class}, exportSchema = false)
- public abstract static class GenericDb extends RoomDatabase {
- abstract GenericDao getDao();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/IdentityDetectionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/IdentityDetectionTest.java
deleted file mode 100644
index e00a778..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/IdentityDetectionTest.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * 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.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.fail;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-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 android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class IdentityDetectionTest {
- static final String TAG = "IdentityDetectionTest";
- static final String DB_FILE_NAME = "identity_test_db";
- TestDatabase mTestDatabase;
- @Before
- public void createTestDatabase() {
- deleteDbFile();
- }
-
- @Test
- public void reOpenWithoutIssues() {
- openDb();
- mTestDatabase.getUserDao().insert(TestUtil.createUser(3));
- closeDb();
- openDb();
- User[] users = mTestDatabase.getUserDao().loadByIds(3);
- assertThat(users.length, is(1));
- }
-
- @Test
- public void reOpenChangedHash() {
- openDb();
- mTestDatabase.getUserDao().insert(TestUtil.createUser(3));
- // change the hash
- SupportSQLiteDatabase db = mTestDatabase.getOpenHelper().getWritableDatabase();
- db.execSQL("UPDATE " + Room.MASTER_TABLE_NAME + " SET `identity_hash` = ?"
- + " WHERE id = 42", new String[]{"bad hash"});
- closeDb();
- Throwable[] exceptions = new Throwable[1];
- try {
- openDb();
- mTestDatabase.getUserDao().loadByIds(3);
- } catch (Throwable t) {
- exceptions[0] = t;
- mTestDatabase = null;
- }
- assertThat(exceptions[0], instanceOf(IllegalStateException.class));
- }
-
- @Test
- public void reOpenMasterTableDropped() {
- openDb();
- mTestDatabase.getUserDao().insert(TestUtil.createUser(3));
- // drop the master table
- SupportSQLiteDatabase db = mTestDatabase.getOpenHelper().getWritableDatabase();
- db.execSQL("DROP TABLE " + Room.MASTER_TABLE_NAME);
- closeDb();
- try {
- openDb();
- mTestDatabase.getUserDao().loadByIds(3);
- fail("Was expecting an exception.");
- } catch (Throwable t) {
- assertThat(t, instanceOf(IllegalStateException.class));
- }
- }
-
- private void closeDb() {
- mTestDatabase.close();
- }
-
- private void openDb() {
- mTestDatabase = Room.databaseBuilder(InstrumentationRegistry.getTargetContext(),
- TestDatabase.class, DB_FILE_NAME).build();
- }
-
- @After
- public void clear() {
- try {
- if (mTestDatabase != null) {
- closeDb();
- }
- deleteDbFile();
- } catch (Throwable t) {
- Log.e(TAG, "could not close test database", t);
- throw t;
- }
- }
-
- private void deleteDbFile() {
- File testDb = InstrumentationRegistry.getTargetContext().getDatabasePath(DB_FILE_NAME);
- testDb.delete();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/IndexingTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/IndexingTest.java
deleted file mode 100644
index 16f916b..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/IndexingTest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import android.arch.persistence.room.ColumnInfo;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.Index;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class IndexingTest {
- @Entity(
- tableName = "foo_table",
- indices = {
- @Index({"field1", "field2"}),
- @Index(value = {"field2", "mId"}, unique = true),
- @Index(value = {"field2"}, unique = true, name = "customIndex"),
- })
- static class Entity1 {
- @PrimaryKey
- public int mId;
- public String field1;
- public String field2;
- @ColumnInfo(index = true, name = "my_field")
- public String field3;
- }
-
- static class IndexInfo {
- public String name;
- @ColumnInfo(name = "tbl_name")
- public String tableName;
- public String sql;
- }
-
- @Dao
- public interface SqlMasterDao {
- @Query("SELECT * FROM sqlite_master WHERE type = 'index'")
- List<IndexInfo> loadIndices();
- }
-
- @Database(entities = {Entity1.class}, version = 1, exportSchema = false)
- abstract static class IndexingDb extends RoomDatabase {
- abstract SqlMasterDao sqlMasterDao();
- }
-
- @Test
- public void verifyIndices() {
- Context context = InstrumentationRegistry.getTargetContext();
- IndexingDb db = Room.inMemoryDatabaseBuilder(context, IndexingDb.class).build();
- List<IndexInfo> indices = db.sqlMasterDao().loadIndices();
- assertThat(indices.size(), is(4));
- for (IndexInfo info : indices) {
- assertThat(info.tableName, is("foo_table"));
- }
- assertThat(indices.get(0).sql, is("CREATE INDEX `index_foo_table_field1_field2`"
- + " ON `foo_table` (`field1`, `field2`)"));
- assertThat(indices.get(1).sql, is("CREATE UNIQUE INDEX `index_foo_table_field2_mId`"
- + " ON `foo_table` (`field2`, `mId`)"));
- assertThat(indices.get(2).sql, is("CREATE UNIQUE INDEX `customIndex`"
- + " ON `foo_table` (`field2`)"));
- assertThat(indices.get(3).sql, is("CREATE INDEX `index_foo_table_my_field`"
- + " ON `foo_table` (`my_field`)"));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java
deleted file mode 100644
index 33f4018..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.hasItem;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
-
-import android.arch.core.executor.testing.CountingTaskExecutorRule;
-import android.arch.persistence.room.InvalidationTracker;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Tests invalidation tracking.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InvalidationTest {
- @Rule
- public CountingTaskExecutorRule executorRule = new CountingTaskExecutorRule();
- private UserDao mUserDao;
- private TestDatabase mDb;
-
- @Before
- public void createDb() throws TimeoutException, InterruptedException {
- Context context = InstrumentationRegistry.getTargetContext();
- mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
- mUserDao = mDb.getUserDao();
- drain();
- }
-
- @After
- public void closeDb() throws TimeoutException, InterruptedException {
- mDb.close();
- drain();
- }
-
- private void drain() throws TimeoutException, InterruptedException {
- executorRule.drainTasks(1, TimeUnit.MINUTES);
- }
-
- @Test
- public void testInvalidationOnUpdate() throws InterruptedException, TimeoutException {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- LoggingObserver observer = new LoggingObserver("User");
- mDb.getInvalidationTracker().addObserver(observer);
- drain();
- mUserDao.updateById(3, "foo2");
- drain();
- assertThat(observer.getInvalidatedTables(), hasSize(1));
- assertThat(observer.getInvalidatedTables(), hasItem("User"));
- }
-
- @Test
- public void testInvalidationOnDelete() throws InterruptedException, TimeoutException {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- LoggingObserver observer = new LoggingObserver("User");
- mDb.getInvalidationTracker().addObserver(observer);
- drain();
- mUserDao.delete(user);
- drain();
- assertThat(observer.getInvalidatedTables(), hasSize(1));
- assertThat(observer.getInvalidatedTables(), hasItem("User"));
- }
-
- @Test
- public void testInvalidationOnInsert() throws InterruptedException, TimeoutException {
- LoggingObserver observer = new LoggingObserver("User");
- mDb.getInvalidationTracker().addObserver(observer);
- drain();
- mUserDao.insert(TestUtil.createUser(3));
- drain();
- assertThat(observer.getInvalidatedTables(), hasSize(1));
- assertThat(observer.getInvalidatedTables(), hasItem("User"));
- }
-
- @Test
- public void testDontInvalidateOnLateInsert() throws InterruptedException, TimeoutException {
- LoggingObserver observer = new LoggingObserver("User");
- mUserDao.insert(TestUtil.createUser(3));
- drain();
- mDb.getInvalidationTracker().addObserver(observer);
- drain();
- assertThat(observer.getInvalidatedTables(), nullValue());
- }
-
- @Test
- public void testMultipleTables() throws InterruptedException, TimeoutException {
- LoggingObserver observer = new LoggingObserver("User", "Pet");
- mDb.getInvalidationTracker().addObserver(observer);
- drain();
- mUserDao.insert(TestUtil.createUser(3));
- drain();
- assertThat(observer.getInvalidatedTables(), hasSize(1));
- assertThat(observer.getInvalidatedTables(), hasItem("User"));
- }
-
- private static class LoggingObserver extends InvalidationTracker.Observer {
- private Set<String> mInvalidatedTables;
-
- LoggingObserver(String... tables) {
- super(tables);
- }
-
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- mInvalidatedTables = tables;
- }
-
- Set<String> getInvalidatedTables() {
- return mInvalidatedTables;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
deleted file mode 100644
index d073598..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
+++ /dev/null
@@ -1,405 +0,0 @@
-/*
- * 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.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.persistence.room.InvalidationTrackerTrojan;
-import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
-import android.arch.persistence.room.integration.testapp.vo.Pet;
-import android.arch.persistence.room.integration.testapp.vo.PetsToys;
-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;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Tests invalidation tracking.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LiveDataQueryTest extends TestDatabaseTest {
- @Rule
- public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
-
- @Test
- public void observeById() throws InterruptedException, ExecutionException, TimeoutException {
- final LiveData<User> userLiveData = mUserDao.liveUserById(5);
- final TestLifecycleOwner testOwner = new TestLifecycleOwner();
- testOwner.handleEvent(Lifecycle.Event.ON_CREATE);
- final TestObserver<User> observer = new TestObserver<>();
- observe(userLiveData, testOwner, observer);
- assertThat(observer.hasValue(), is(false));
- observer.reset();
-
- testOwner.handleEvent(Lifecycle.Event.ON_START);
- assertThat(observer.get(), is(nullValue()));
-
- // another id
- observer.reset();
- mUserDao.insert(TestUtil.createUser(7));
- assertThat(observer.get(), is(nullValue()));
-
- observer.reset();
- final User u5 = TestUtil.createUser(5);
- mUserDao.insert(u5);
- assertThat(observer.get(), is(notNullValue()));
-
- u5.setName("foo-foo-foo");
- observer.reset();
- mUserDao.insertOrReplace(u5);
- final User updated = observer.get();
- assertThat(updated, is(notNullValue()));
- assertThat(updated.getName(), is("foo-foo-foo"));
-
- testOwner.handleEvent(Lifecycle.Event.ON_STOP);
- observer.reset();
- u5.setName("baba");
- mUserDao.insertOrReplace(u5);
- assertThat(observer.hasValue(), is(false));
- }
-
- @Test
- public void observeListQuery() throws InterruptedException, ExecutionException,
- TimeoutException {
- final LiveData<List<User>> userLiveData = mUserDao.liveUsersListByName("frida");
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- final TestObserver<List<User>> observer = new TestObserver<>();
- observe(userLiveData, lifecycleOwner, observer);
- assertThat(observer.get(), is(Collections.<User>emptyList()));
-
- observer.reset();
- final User user1 = TestUtil.createUser(3);
- user1.setName("dog frida");
- mUserDao.insert(user1);
- assertThat(observer.get(), is(Collections.singletonList(user1)));
-
- observer.reset();
- final User user2 = TestUtil.createUser(5);
- user2.setName("does not match");
- mUserDao.insert(user2);
- assertThat(observer.get(), is(Collections.singletonList(user1)));
-
- observer.reset();
- user1.setName("i don't match either");
- mUserDao.insertOrReplace(user1);
- assertThat(observer.get(), is(Collections.<User>emptyList()));
-
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_STOP);
-
- observer.reset();
- final User user3 = TestUtil.createUser(9);
- user3.setName("painter frida");
- mUserDao.insertOrReplace(user3);
- assertThat(observer.hasValue(), is(false));
-
- observer.reset();
- final User user4 = TestUtil.createUser(11);
- user4.setName("friday");
- mUserDao.insertOrReplace(user4);
- assertThat(observer.hasValue(), is(false));
-
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- assertThat(observer.get(), is(Arrays.asList(user4, user3)));
- }
-
- @Test
- public void liveDataWithPojo() throws ExecutionException, InterruptedException,
- TimeoutException {
- User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
- users[0].setAge(10);
- users[0].setWeight(15);
-
- users[1].setAge(20);
- users[1].setWeight(25);
-
- users[2].setAge(20);
- users[2].setWeight(26);
-
- users[3].setAge(10);
- users[3].setWeight(21);
-
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
-
- final TestObserver<AvgWeightByAge> observer = new TestObserver<>();
- LiveData<AvgWeightByAge> liveData = mUserDao.maxWeightByAgeGroup();
-
- observe(liveData, lifecycleOwner, observer);
- assertThat(observer.get(), is(nullValue()));
-
- observer.reset();
- mUserDao.insertAll(users);
- assertThat(observer.get(), is(new AvgWeightByAge(20, 25.5f)));
-
- observer.reset();
- User user3 = mUserDao.load(3);
- user3.setWeight(79);
- mUserDao.insertOrReplace(user3);
-
- assertThat(observer.get(), is(new AvgWeightByAge(10, 50)));
- }
-
- @Test
- public void withRelation() throws ExecutionException, InterruptedException, TimeoutException {
- final LiveData<UserAndAllPets> liveData = mUserPetDao.liveUserWithPets(3);
- final TestObserver<UserAndAllPets> observer = new TestObserver<>();
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- observe(liveData, lifecycleOwner, observer);
- assertThat(observer.get(), is(nullValue()));
-
- observer.reset();
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- final UserAndAllPets noPets = observer.get();
- assertThat(noPets.user, is(user));
-
- observer.reset();
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
- mPetDao.insertAll(pets);
-
- final UserAndAllPets withPets = observer.get();
- assertThat(withPets.user, is(user));
- assertThat(withPets.pets, is(Arrays.asList(pets)));
- }
-
- @Test
- public void withRelationOnly() throws ExecutionException, InterruptedException,
- TimeoutException {
- LiveData<PetsToys> liveData = mSpecificDogDao.getSpecificDogsToys();
-
- PetsToys expected = new PetsToys();
- expected.petId = 123;
-
- Toy toy = new Toy();
- toy.setId(1);
- toy.setPetId(123);
- toy.setName("ball");
-
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- final TestObserver<PetsToys> observer = new TestObserver<>();
- observe(liveData, lifecycleOwner, observer);
- assertThat(observer.get(), is(expected));
-
- observer.reset();
- expected.toys.add(toy);
- mToyDao.insert(toy);
- assertThat(observer.get(), is(expected));
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
- public void withWithClause() throws ExecutionException, InterruptedException,
- TimeoutException {
- LiveData<List<String>> actual =
- mWithClauseDao.getUsersWithFactorialIdsLiveData(0);
- List<String> expected = new ArrayList<>();
-
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- final TestObserver<List<String>> observer = new TestObserver<>();
- observe(actual, lifecycleOwner, observer);
- assertThat(observer.get(), is(expected));
-
- observer.reset();
- User user = new User();
- user.setId(0);
- user.setName("Zero");
- mUserDao.insert(user);
- assertThat(observer.get(), is(expected));
-
- observer.reset();
- user = new User();
- user.setId(1);
- user.setName("One");
- mUserDao.insert(user);
- expected.add("One");
- assertThat(observer.get(), is(expected));
-
- observer.reset();
- user = new User();
- user.setId(6);
- user.setName("Six");
- mUserDao.insert(user);
- assertThat(observer.get(), is(expected));
-
- actual = mWithClauseDao.getUsersWithFactorialIdsLiveData(3);
- observe(actual, lifecycleOwner, observer);
- expected.add("Six");
- assertThat(observer.get(), is(expected));
- }
-
- @MediumTest
- @Test
- public void handleGc() throws ExecutionException, InterruptedException, TimeoutException {
- LiveData<User> liveData = mUserDao.liveUserById(3);
- final TestObserver<User> observer = new TestObserver<>();
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- observe(liveData, lifecycleOwner, observer);
- assertThat(observer.get(), is(nullValue()));
- observer.reset();
- final User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- assertThat(observer.get(), is(notNullValue()));
- observer.reset();
- forceGc();
- String name = UUID.randomUUID().toString();
- mUserDao.updateById(3, name);
- assertThat(observer.get().getName(), is(name));
-
- // release references
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY);
- }
- });
- WeakReference<LiveData> weakLiveData = new WeakReference<LiveData>(liveData);
- //noinspection UnusedAssignment
- liveData = null;
- forceGc();
- mUserDao.updateById(3, "Bar");
- forceGc();
- assertThat(InvalidationTrackerTrojan.countObservers(mDatabase.getInvalidationTracker()),
- is(0));
- assertThat(weakLiveData.get(), nullValue());
- }
-
- @Test
- public void booleanLiveData() throws ExecutionException, InterruptedException,
- TimeoutException {
- User user = TestUtil.createUser(3);
- user.setAdmin(false);
- LiveData<Boolean> adminLiveData = mUserDao.isAdminLiveData(3);
- final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
- lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
- final TestObserver<Boolean> observer = new TestObserver<>();
- observe(adminLiveData, lifecycleOwner, observer);
- assertThat(observer.get(), is(nullValue()));
- mUserDao.insert(user);
- assertThat(observer.get(), is(false));
- user.setAdmin(true);
- mUserDao.insertOrReplace(user);
- assertThat(observer.get(), is(true));
- }
-
- private void observe(final LiveData liveData, final LifecycleOwner provider,
- final Observer observer) throws ExecutionException, InterruptedException {
- FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- //noinspection unchecked
- liveData.observe(provider, observer);
- return null;
- }
- });
- ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
- futureTask.get();
- }
-
- private void drain() throws TimeoutException, InterruptedException {
- mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
- }
-
- private static void forceGc() {
- Runtime.getRuntime().gc();
- Runtime.getRuntime().runFinalization();
- Runtime.getRuntime().gc();
- Runtime.getRuntime().runFinalization();
- }
-
- static class TestLifecycleOwner implements LifecycleOwner {
-
- private LifecycleRegistry mLifecycle;
-
- TestLifecycleOwner() {
- mLifecycle = new LifecycleRegistry(this);
- }
-
- @Override
- public Lifecycle getLifecycle() {
- return mLifecycle;
- }
-
- void handleEvent(Lifecycle.Event event) {
- mLifecycle.handleLifecycleEvent(event);
- }
- }
-
- private class TestObserver<T> implements Observer<T> {
- private T mLastData;
- private boolean mHasValue = false;
-
- void reset() {
- mHasValue = false;
- mLastData = null;
- }
-
- @Override
- public void onChanged(@Nullable T o) {
- mLastData = o;
- mHasValue = true;
- }
-
- boolean hasValue() throws TimeoutException, InterruptedException {
- drain();
- return mHasValue;
- }
-
- T get() throws InterruptedException, TimeoutException {
- drain();
- assertThat(hasValue(), is(true));
- return mLastData;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/MainThreadCheckTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/MainThreadCheckTest.java
deleted file mode 100644
index f4ed5dd..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/MainThreadCheckTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.instanceOf;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import android.arch.core.util.Function;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MainThreadCheckTest {
-
- @Test
- public void testMainThread() {
- final Throwable error = test(false, new Function<TestDatabase, Void>() {
- @Override
- public Void apply(TestDatabase db) {
- db.getUserDao().load(3);
- return null;
- }
- });
- assertThat(error, notNullValue());
- assertThat(error, instanceOf(IllegalStateException.class));
- }
-
- @Test
- public void testFlowableOnMainThread() {
- final Throwable error = test(false, new Function<TestDatabase, Void>() {
- @Override
- public Void apply(TestDatabase db) {
- db.getUserDao().flowableUserById(3);
- return null;
- }
- });
- assertThat(error, nullValue());
- }
-
- @Test
- public void testLiveDataOnMainThread() {
- final Throwable error = test(false, new Function<TestDatabase, Void>() {
- @Override
- public Void apply(TestDatabase db) {
- db.getUserDao().liveUserById(3);
- return null;
- }
- });
- assertThat(error, nullValue());
- }
-
- @Test
- public void testAllowMainThread() {
- final Throwable error = test(true, new Function<TestDatabase, Void>() {
- @Override
- public Void apply(TestDatabase db) {
- db.getUserDao().load(3);
- return null;
- }
- });
- assertThat(error, nullValue());
- }
-
- private Throwable test(boolean allowMainThread, final Function<TestDatabase, Void> fun) {
- Context context = InstrumentationRegistry.getTargetContext();
- final RoomDatabase.Builder<TestDatabase> builder = Room.inMemoryDatabaseBuilder(
- context, TestDatabase.class);
- if (allowMainThread) {
- builder.allowMainThreadQueries();
- }
- final TestDatabase db = builder.build();
- final AtomicReference<Throwable> error = new AtomicReference<>();
- try {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- try {
- fun.apply(db);
- } catch (Throwable t) {
- error.set(t);
- }
- }
- });
- } finally {
- db.close();
- }
- return error.get();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java
deleted file mode 100644
index b1579fc..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class PojoTest {
- private UserDao mUserDao;
-
- @Before
- public void createDb() {
- Context context = InstrumentationRegistry.getTargetContext();
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
- mUserDao = db.getUserDao();
- }
-
- @Test
- public void weightsByAge() {
- User[] users = TestUtil.createUsersArray(3, 5, 7, 10);
- users[0].setAge(10);
- users[0].setWeight(20);
-
- users[1].setAge(10);
- users[1].setWeight(30);
-
- users[2].setAge(15);
- users[2].setWeight(12);
-
- users[3].setAge(35);
- users[3].setWeight(55);
-
- mUserDao.insertAll(users);
- assertThat(mUserDao.weightByAge(), is(
- Arrays.asList(
- new AvgWeightByAge(35, 55),
- new AvgWeightByAge(10, 25),
- new AvgWeightByAge(15, 12)
- )
- ));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
deleted file mode 100644
index c3ebfe9..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-
-import android.arch.persistence.room.integration.testapp.vo.EmbeddedUserAndAllPets;
-import android.arch.persistence.room.integration.testapp.vo.Pet;
-import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
-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.arch.persistence.room.integration.testapp.vo.UserAndPetAdoptionDates;
-import android.arch.persistence.room.integration.testapp.vo.UserIdAndPetNames;
-import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class PojoWithRelationTest extends TestDatabaseTest {
- @Test
- public void fetchAll() {
- User[] users = TestUtil.createUsersArray(1, 2, 3);
- Pet[][] userPets = new Pet[3][];
- mUserDao.insertAll(users);
- for (User user : users) {
- Pet[] pets = TestUtil.createPetsForUser(user.getId(), user.getId() * 10,
- user.getId() - 1);
- mPetDao.insertAll(pets);
- userPets[user.getId() - 1] = pets;
- }
- List<UserAndAllPets> usersAndPets = mUserPetDao.loadAllUsersWithTheirPets();
- assertThat(usersAndPets.size(), is(3));
- assertThat(usersAndPets.get(0).user, is(users[0]));
- assertThat(usersAndPets.get(0).pets, is(Collections.<Pet>emptyList()));
-
- assertThat(usersAndPets.get(1).user, is(users[1]));
- assertThat(usersAndPets.get(1).pets, is(Arrays.asList(userPets[1])));
-
- assertThat(usersAndPets.get(2).user, is(users[2]));
- assertThat(usersAndPets.get(2).pets, is(Arrays.asList(userPets[2])));
- }
-
- private void createData() {
- User[] users = TestUtil.createUsersArray(1, 2);
- mUserDao.insertAll(users);
- Pet user1_pet1 = TestUtil.createPet(1);
- user1_pet1.setUserId(1);
- user1_pet1.setName("pet1");
- mPetDao.insertOrReplace(user1_pet1);
-
- Pet user1_pet2 = TestUtil.createPet(2);
- user1_pet2.setUserId(1);
- user1_pet2.setName("pet2");
- mPetDao.insertOrReplace(user1_pet2);
-
- Pet user2_pet1 = TestUtil.createPet(3);
- user2_pet1.setUserId(2);
- user2_pet1.setName("pet3");
- mPetDao.insertOrReplace(user2_pet1);
- }
-
- @Test
- public void fetchWithNames() {
- createData();
-
- List<UserIdAndPetNames> usersAndPets = mUserPetDao.loadUserAndPetNames();
- assertThat(usersAndPets.size(), is(2));
- assertThat(usersAndPets.get(0).userId, is(1));
- assertThat(usersAndPets.get(1).userId, is(2));
- assertThat(usersAndPets.get(0).names, is(Arrays.asList("pet1", "pet2")));
- assertThat(usersAndPets.get(1).names, is(Collections.singletonList("pet3")));
- }
-
- @Test
- public void nested() {
- createData();
- Toy pet1_toy1 = new Toy();
- pet1_toy1.setName("toy1");
- pet1_toy1.setPetId(1);
- Toy pet1_toy2 = new Toy();
- pet1_toy2.setName("toy2");
- pet1_toy2.setPetId(1);
- mToyDao.insert(pet1_toy1, pet1_toy2);
- List<UserWithPetsAndToys> userWithPetsAndToys = mUserPetDao.loadUserWithPetsAndToys();
- assertThat(userWithPetsAndToys.size(), is(2));
- UserWithPetsAndToys first = userWithPetsAndToys.get(0);
- List<Toy> toys = first.pets.get(0).toys;
- assertThat(toys.get(0).getName(), is("toy1"));
- assertThat(toys.get(1).getName(), is("toy2"));
- assertThat(userWithPetsAndToys.get(1).pets.get(0).toys, is(Collections.<Toy>emptyList()));
- }
-
- @Test
- public void duplicateParentField() {
- User[] users = TestUtil.createUsersArray(1, 2);
- Pet[] pets_1 = TestUtil.createPetsForUser(1, 1, 2);
- Pet[] pets_2 = TestUtil.createPetsForUser(2, 10, 1);
- mUserDao.insertAll(users);
- mPetDao.insertAll(pets_1);
- mPetDao.insertAll(pets_2);
- List<UserAndAllPets> userAndAllPets = mUserPetDao.unionByItself();
- assertThat(userAndAllPets.size(), is(4));
- for (int i = 0; i < 4; i++) {
- assertThat("user at " + i, userAndAllPets.get(i).user, is(users[i % 2]));
- }
- assertThat(userAndAllPets.get(0).pets, is(Arrays.asList(pets_1)));
- assertThat(userAndAllPets.get(2).pets, is(Arrays.asList(pets_1)));
-
- assertThat(userAndAllPets.get(1).pets, is(Arrays.asList(pets_2)));
- assertThat(userAndAllPets.get(3).pets, is(Arrays.asList(pets_2)));
- }
-
- @Test
- public void embeddedRelation() {
- createData();
- EmbeddedUserAndAllPets relationContainer = mUserPetDao.loadUserAndPetsAsEmbedded(1);
- assertThat(relationContainer.getUserAndAllPets(), notNullValue());
- assertThat(relationContainer.getUserAndAllPets().user.getId(), is(1));
- assertThat(relationContainer.getUserAndAllPets().pets.size(), is(2));
- }
-
- @Test
- public void boxedPrimitiveList() {
- Pet pet1 = TestUtil.createPet(3);
- Pet pet2 = TestUtil.createPet(5);
-
- Toy pet1_toy1 = TestUtil.createToyForPet(pet1, 10);
- Toy pet1_toy2 = TestUtil.createToyForPet(pet1, 20);
- Toy pet2_toy1 = TestUtil.createToyForPet(pet2, 30);
-
- mPetDao.insertOrReplace(pet1, pet2);
- mToyDao.insert(pet1_toy1, pet1_toy2, pet2_toy1);
-
- List<PetWithToyIds> petWithToyIds = mPetDao.allPetsWithToyIds();
- //noinspection ArraysAsListWithZeroOrOneArgument
- assertThat(petWithToyIds, is(
- Arrays.asList(
- new PetWithToyIds(pet1, Arrays.asList(10, 20)),
- new PetWithToyIds(pet2, Arrays.asList(30)))
- ));
- }
-
- @Test
- public void viaTypeConverter() {
- User user = TestUtil.createUser(3);
- Pet pet1 = TestUtil.createPet(3);
- Date date1 = new Date(300);
- pet1.setAdoptionDate(date1);
- Pet pet2 = TestUtil.createPet(5);
- Date date2 = new Date(700);
- pet2.setAdoptionDate(date2);
-
- pet1.setUserId(3);
- pet2.setUserId(3);
- mUserDao.insert(user);
- mPetDao.insertOrReplace(pet1, pet2);
-
- List<UserAndPetAdoptionDates> adoptions =
- mUserPetDao.loadUserWithPetAdoptionDates();
-
- assertThat(adoptions, is(Arrays.asList(
- new UserAndPetAdoptionDates(user, Arrays.asList(new Date(300), new Date(700)))
- )));
- }
-
- @Test
- public void largeRelation_child() {
- User user = TestUtil.createUser(3);
- List<Pet> pets = new ArrayList<>();
- for (int i = 0; i < 2000; i++) {
- Pet pet = TestUtil.createPet(i + 1);
- pet.setUserId(3);
- }
- mUserDao.insert(user);
- mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
- List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
- assertThat(result.size(), is(1));
- assertThat(result.get(0).user, is(user));
- assertThat(result.get(0).pets, is(pets));
- }
-
- @Test
- public void largeRelation_parent() {
- final List<User> users = new ArrayList<>();
- final List<Pet> pets = new ArrayList<>();
- for (int i = 0; i < 2000; i++) {
- User user = TestUtil.createUser(i + 1);
- users.add(user);
- Pet pet = TestUtil.createPet(i + 1);
- pet.setUserId(user.getId());
- pets.add(pet);
- }
- mDatabase.runInTransaction(new Runnable() {
- @Override
- public void run() {
- mUserDao.insertAll(users.toArray(new User[users.size()]));
- mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
- }
- });
- List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
- assertThat(result.size(), is(2000));
- for (int i = 0; i < 2000; i++) {
- assertThat(result.get(i).user, is(users.get(i)));
- assertThat(result.get(i).pets, is(Collections.singletonList(pets.get(i))));
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java
deleted file mode 100644
index fda4373..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-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(),
- PKeyTestDatabase.class).build();
- }
-
- @Test
- public void integerTest() {
- IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
- entity.data = "foo";
- mDatabase.integerAutoIncPKeyDao().insertMe(entity);
- IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(1);
- assertThat(loaded, notNullValue());
- assertThat(loaded.data, is(entity.data));
- }
-
- @Test
- public void dontOverrideNullable0() {
- IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
- entity.pKey = 0;
- entity.data = "foo";
- mDatabase.integerAutoIncPKeyDao().insertMe(entity);
- IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(0);
- assertThat(loaded, notNullValue());
- assertThat(loaded.data, is(entity.data));
- }
-
- @Test
- public void intTest() {
- IntAutoIncPKeyEntity entity = new IntAutoIncPKeyEntity();
- entity.data = "foo";
- mDatabase.intPKeyDao().insertMe(entity);
- IntAutoIncPKeyEntity loaded = mDatabase.intPKeyDao().getMe(1);
- assertThat(loaded, notNullValue());
- assertThat(loaded.data, is(entity.data));
- }
-
- @Test
- public void getInsertedId() {
- IntAutoIncPKeyEntity entity = new IntAutoIncPKeyEntity();
- entity.data = "foo";
- final long id = mDatabase.intPKeyDao().insertAndGetId(entity);
- assertThat(mDatabase.intPKeyDao().getMe((int) id).data, is("foo"));
- }
-
- @Test
- public void getInsertedIds() {
- IntAutoIncPKeyEntity entity = new IntAutoIncPKeyEntity();
- entity.data = "foo";
- IntAutoIncPKeyEntity entity2 = new IntAutoIncPKeyEntity();
- entity2.data = "foo2";
- final long[] ids = mDatabase.intPKeyDao().insertAndGetIds(entity, entity2);
- assertThat(mDatabase.intPKeyDao().loadDataById(ids), is(Arrays.asList("foo", "foo2")));
- }
-
- @Test
- public void getInsertedIdFromInteger() {
- IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
- entity.data = "foo";
- final long id = mDatabase.integerAutoIncPKeyDao().insertAndGetId(entity);
- assertThat(mDatabase.integerAutoIncPKeyDao().getMe((int) id).data, is("foo"));
- }
-
- @Test
- public void getInsertedIdsFromInteger() {
- IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
- entity.data = "foo";
- IntegerAutoIncPKeyEntity entity2 = new IntegerAutoIncPKeyEntity();
- entity2.data = "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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
deleted file mode 100644
index 5b9adfe..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.executor.testing.CountingTaskExecutorRule;
-import android.arch.lifecycle.Lifecycle;
-import android.arch.lifecycle.LiveData;
-import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListBuilder;
-import android.arch.paging.PagedList;
-import android.arch.paging.PositionalDataSource;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.Ignore;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Relation;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.RoomWarnings;
-import android.arch.persistence.room.Transaction;
-import android.arch.persistence.room.paging.LimitOffsetDataSource;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import io.reactivex.Flowable;
-import io.reactivex.Maybe;
-import io.reactivex.Single;
-import io.reactivex.observers.TestObserver;
-import io.reactivex.schedulers.Schedulers;
-import io.reactivex.subscribers.TestSubscriber;
-
-@SmallTest
-@RunWith(Parameterized.class)
-@SuppressWarnings("CheckReturnValue")
-public class QueryTransactionTest {
- @Rule
- public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule();
- private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0);
- private TransactionDb mDb;
- private final boolean mUseTransactionDao;
- private Entity1Dao mDao;
- private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest
- .TestLifecycleOwner();
-
- @NonNull
- @Parameterized.Parameters(name = "useTransaction_{0}")
- public static Boolean[] getParams() {
- return new Boolean[]{false, true};
- }
-
- public QueryTransactionTest(boolean useTransactionDao) {
- mUseTransactionDao = useTransactionDao;
- }
-
- @Before
- public void initDb() {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START));
-
- resetTransactionCount();
- mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
- TransactionDb.class).build();
- mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao();
- drain();
- }
-
- @After
- public void closeDb() {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY));
- drain();
- mDb.close();
- }
-
- @Test
- public void readList() {
- mDao.insert(new Entity1(1, "foo"));
- resetTransactionCount();
-
- int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
- List<Entity1> allEntities = mDao.allEntities();
- assertTransactionCount(allEntities, expectedTransactionCount);
- }
-
- @Test
- public void liveData() {
- LiveData<List<Entity1>> listLiveData = mDao.liveData();
- observeForever(listLiveData);
- drain();
- assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList()));
-
- resetTransactionCount();
- mDao.insert(new Entity1(1, "foo"));
- drain();
-
- //noinspection ConstantConditions
- assertThat(listLiveData.getValue().size(), is(1));
- int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
- assertTransactionCount(listLiveData.getValue(), expectedTransactionCount);
- }
-
- @Test
- public void flowable() {
- Flowable<List<Entity1>> flowable = mDao.flowable();
- TestSubscriber<List<Entity1>> subscriber = observe(flowable);
- drain();
- assertThat(subscriber.values().size(), is(1));
-
- resetTransactionCount();
- mDao.insert(new Entity1(1, "foo"));
- drain();
-
- List<Entity1> allEntities = subscriber.values().get(1);
- assertThat(allEntities.size(), is(1));
- int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
- assertTransactionCount(allEntities, expectedTransactionCount);
- }
-
- @Test
- public void maybe() {
- mDao.insert(new Entity1(1, "foo"));
- resetTransactionCount();
-
- int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
- Maybe<List<Entity1>> listMaybe = mDao.maybe();
- TestObserver<List<Entity1>> observer = observe(listMaybe);
- drain();
- List<Entity1> allEntities = observer.values().get(0);
- assertTransactionCount(allEntities, expectedTransactionCount);
- }
-
- @Test
- public void single() {
- mDao.insert(new Entity1(1, "foo"));
- resetTransactionCount();
-
- int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
- Single<List<Entity1>> listMaybe = mDao.single();
- TestObserver<List<Entity1>> observer = observe(listMaybe);
- drain();
- List<Entity1> allEntities = observer.values().get(0);
- assertTransactionCount(allEntities, expectedTransactionCount);
- }
-
- @Test
- public void relation() {
- mDao.insert(new Entity1(1, "foo"));
- mDao.insert(new Child(1, 1));
- mDao.insert(new Child(2, 1));
- resetTransactionCount();
-
- List<Entity1WithChildren> result = mDao.withRelation();
- int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
- assertTransactionCountWithChildren(result, expectedTransactionCount);
- }
-
- @Test
- public void pagedList() {
- LiveData<PagedList<Entity1>> pagedList =
- new LivePagedListBuilder<>(mDao.pagedList(), 10).build();
- observeForever(pagedList);
- drain();
- assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0));
-
- mDao.insert(new Entity1(1, "foo"));
- drain();
- //noinspection ConstantConditions
- assertThat(pagedList.getValue().size(), is(1));
- assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 2 : 1);
-
- mDao.insert(new Entity1(2, "bar"));
- drain();
- assertThat(pagedList.getValue().size(), is(2));
- assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 4 : 2);
- }
-
- @Test
- public void dataSource() {
- mDao.insert(new Entity1(2, "bar"));
- drain();
- resetTransactionCount();
- @SuppressWarnings("deprecation")
- LimitOffsetDataSource<Entity1> dataSource =
- (LimitOffsetDataSource<Entity1>) mDao.dataSource();
- dataSource.loadRange(0, 10);
- assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
- }
-
- private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) {
- assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
- assertThat(allEntities.isEmpty(), is(false));
- for (Entity1 entity1 : allEntities) {
- assertThat(entity1.transactionId, is(expectedTransactionCount));
- }
- }
-
- private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities,
- int expectedTransactionCount) {
- assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
- assertThat(allEntities.isEmpty(), is(false));
- for (Entity1WithChildren entity1 : allEntities) {
- assertThat(entity1.transactionId, is(expectedTransactionCount));
- assertThat(entity1.children, notNullValue());
- assertThat(entity1.children.isEmpty(), is(false));
- for (Child child : entity1.children) {
- assertThat(child.transactionId, is(expectedTransactionCount));
- }
- }
- }
-
- private void resetTransactionCount() {
- sStartedTransactionCount.set(0);
- }
-
- private void drain() {
- try {
- countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- throw new AssertionError("interrupted", e);
- } catch (TimeoutException e) {
- throw new AssertionError("drain timed out", e);
- }
- }
-
- private <T> TestSubscriber<T> observe(final Flowable<T> flowable) {
- TestSubscriber<T> subscriber = new TestSubscriber<>();
- flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
- .subscribeWith(subscriber);
- return subscriber;
- }
-
- private <T> TestObserver<T> observe(final Maybe<T> maybe) {
- TestObserver<T> observer = new TestObserver<>();
- maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
- .subscribeWith(observer);
- return observer;
- }
-
- private <T> TestObserver<T> observe(final Single<T> single) {
- TestObserver<T> observer = new TestObserver<>();
- single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
- .subscribeWith(observer);
- return observer;
- }
-
- private <T> void observeForever(final LiveData<T> liveData) {
- FutureTask<Void> futureTask = new FutureTask<>(() -> {
- liveData.observe(mLifecycleOwner, t -> {
- });
- return null;
- });
- ArchTaskExecutor.getMainThreadExecutor().execute(futureTask);
- try {
- futureTask.get();
- } catch (InterruptedException e) {
- throw new AssertionError("interrupted", e);
- } catch (ExecutionException e) {
- throw new AssertionError("execution error", e);
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- static class Entity1WithChildren extends Entity1 {
- @Relation(entity = Child.class, parentColumn = "id",
- entityColumn = "entity1Id")
- public List<Child> children;
-
- Entity1WithChildren(int id, String value) {
- super(id, value);
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity
- static class Child {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public int entity1Id;
- @Ignore
- public final int transactionId = sStartedTransactionCount.get();
-
- Child(int id, int entity1Id) {
- this.id = id;
- this.entity1Id = entity1Id;
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Entity
- static class Entity1 {
- @PrimaryKey(autoGenerate = true)
- public int id;
- public String value;
- @Ignore
- public final int transactionId = sStartedTransactionCount.get();
-
- Entity1(int id, String value) {
- this.id = id;
- this.value = value;
- }
- }
-
- // we don't support dao inheritance for queries so for now, go with this
- interface Entity1Dao {
- String SELECT_ALL = "select * from Entity1";
-
- List<Entity1> allEntities();
-
- Flowable<List<Entity1>> flowable();
-
- Maybe<List<Entity1>> maybe();
-
- Single<List<Entity1>> single();
-
- LiveData<List<Entity1>> liveData();
-
- List<Entity1WithChildren> withRelation();
-
- DataSource.Factory<Integer, Entity1> pagedList();
-
- PositionalDataSource<Entity1> dataSource();
-
- @Insert
- void insert(Entity1 entity1);
-
- @Insert
- void insert(Child entity1);
- }
-
- @Dao
- interface EntityDao extends Entity1Dao {
- @Override
- @Query(SELECT_ALL)
- List<Entity1> allEntities();
-
- @Override
- @Query(SELECT_ALL)
- Flowable<List<Entity1>> flowable();
-
- @Override
- @Query(SELECT_ALL)
- LiveData<List<Entity1>> liveData();
-
- @Override
- @Query(SELECT_ALL)
- Maybe<List<Entity1>> maybe();
-
- @Override
- @Query(SELECT_ALL)
- Single<List<Entity1>> single();
-
- @Override
- @Query(SELECT_ALL)
- @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
- List<Entity1WithChildren> withRelation();
-
- @Override
- @Query(SELECT_ALL)
- DataSource.Factory<Integer, Entity1> pagedList();
-
- @Override
- @Query(SELECT_ALL)
- PositionalDataSource<Entity1> dataSource();
- }
-
- @Dao
- interface TransactionDao extends Entity1Dao {
- @Override
- @Transaction
- @Query(SELECT_ALL)
- List<Entity1> allEntities();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- Flowable<List<Entity1>> flowable();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- LiveData<List<Entity1>> liveData();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- Maybe<List<Entity1>> maybe();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- Single<List<Entity1>> single();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- List<Entity1WithChildren> withRelation();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- DataSource.Factory<Integer, Entity1> pagedList();
-
- @Override
- @Transaction
- @Query(SELECT_ALL)
- PositionalDataSource<Entity1> dataSource();
- }
-
- @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
- abstract static class TransactionDb extends RoomDatabase {
- abstract EntityDao dao();
-
- abstract TransactionDao transactionDao();
-
- @Override
- public void beginTransaction() {
- super.beginTransaction();
- sStartedTransactionCount.incrementAndGet();
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
deleted file mode 100644
index 67f57ed..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright 2018 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.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import static java.util.Collections.emptyList;
-
-import android.arch.core.executor.testing.CountingTaskExecutorRule;
-import android.arch.lifecycle.LiveData;
-import android.arch.persistence.db.SimpleSQLiteQuery;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.room.integration.testapp.dao.RawDao;
-import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
-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.arch.persistence.room.integration.testapp.vo.UserAndPet;
-import android.support.test.InstrumentationRegistry;
-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.Arrays;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class RawQueryTest extends TestDatabaseTest {
- @Rule
- public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
-
- @Test
- public void entity_null() {
- User user = mRawDao.getUser("SELECT * FROM User WHERE mId = 0");
- assertThat(user, is(nullValue()));
- }
-
- @Test
- public void entity_one() {
- User expected = TestUtil.createUser(3);
- mUserDao.insert(expected);
- User received = mRawDao.getUser("SELECT * FROM User WHERE mId = 3");
- assertThat(received, is(expected));
- }
-
- @Test
- public void entity_list() {
- List<User> expected = TestUtil.createUsersList(1, 2, 3, 4);
- mUserDao.insertAll(expected.toArray(new User[4]));
- List<User> received = mRawDao.getUserList("SELECT * FROM User ORDER BY mId ASC");
- assertThat(received, is(expected));
- }
-
- @Test
- public void entity_liveData_string() throws TimeoutException, InterruptedException {
- SupportSQLiteQuery query = new SimpleSQLiteQuery(
- "SELECT * FROM User WHERE mId = ?",
- new Object[]{3}
- );
- liveDataTest(mRawDao.getUserLiveData(query));
- }
-
- @Test
- public void entity_liveData_supportQuery() throws TimeoutException, InterruptedException {
- liveDataTest(mRawDao.getUserLiveData("SELECT * FROM User WHERE mId = 3"));
- }
-
- private void liveDataTest(
- final LiveData<User> liveData) throws TimeoutException, InterruptedException {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> liveData.observeForever(user -> { }));
- drain();
- assertThat(liveData.getValue(), is(nullValue()));
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- drain();
- assertThat(liveData.getValue(), is(user));
- user.setLastName("cxZ");
- mUserDao.insertOrReplace(user);
- drain();
- assertThat(liveData.getValue(), is(user));
- }
-
- @Test
- public void entity_supportSql() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
- new Object[]{3});
- User received = mRawDao.getUser(query);
- assertThat(received, is(user));
- }
-
- @Test
- public void embedded() {
- User user = TestUtil.createUser(3);
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
- mUserDao.insert(user);
- mPetDao.insertAll(pets);
- UserAndPet received = mRawDao.getUserAndPet(
- "SELECT * FROM User, Pet WHERE User.mId = Pet.mUserId LIMIT 1");
- assertThat(received.getUser(), is(user));
- assertThat(received.getPet(), is(pets[0]));
- }
-
- @Test
- public void relation() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 10);
- mPetDao.insertAll(pets);
- UserAndAllPets result = mRawDao
- .getUserAndAllPets("SELECT * FROM User WHERE mId = 3");
- assertThat(result.user, is(user));
- assertThat(result.pets, is(Arrays.asList(pets)));
- }
-
- @Test
- public void pojo() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- NameAndLastName result =
- mRawDao.getUserNameAndLastName("SELECT * FROM User");
- assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
- }
-
- @Test
- public void pojo_supportSql() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- NameAndLastName result =
- mRawDao.getUserNameAndLastName(new SimpleSQLiteQuery(
- "SELECT * FROM User WHERE mId = ?",
- new Object[]{3}
- ));
- assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
- }
-
- @Test
- public void pojo_typeConverter() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- RawDao.UserNameAndBirthday result = mRawDao.getUserAndBirthday(
- "SELECT mName, mBirthday FROM user LIMIT 1");
- assertThat(result.name, is(user.getName()));
- assertThat(result.birthday, is(user.getBirthday()));
- }
-
- @Test
- public void embedded_nullField() {
- User user = TestUtil.createUser(3);
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
- mUserDao.insert(user);
- mPetDao.insertAll(pets);
- UserAndPet received = mRawDao.getUserAndPet("SELECT * FROM User LIMIT 1");
- assertThat(received.getUser(), is(user));
- assertThat(received.getPet(), is(nullValue()));
- }
-
- @Test
- public void embedded_list() {
- User[] users = TestUtil.createUsersArray(3, 5);
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
- mUserDao.insertAll(users);
- mPetDao.insertAll(pets);
- List<UserAndPet> received = mRawDao.getUserAndPetList(
- "SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
- + " ORDER BY mId ASC, mPetId ASC");
- assertThat(received.size(), is(3));
- // row 0
- assertThat(received.get(0).getUser(), is(users[0]));
- assertThat(received.get(0).getPet(), is(pets[0]));
- // row 1
- assertThat(received.get(1).getUser(), is(users[0]));
- assertThat(received.get(1).getPet(), is(pets[1]));
- // row 2
- assertThat(received.get(2).getUser(), is(users[1]));
- assertThat(received.get(2).getPet(), is(nullValue()));
- }
-
- @Test
- public void count() {
- mUserDao.insertAll(TestUtil.createUsersArray(3, 5, 7, 10));
- int count = mRawDao.count("SELECT COUNT(*) FROM User");
- assertThat(count, is(4));
- }
-
- @Test
- public void embedded_liveData() throws TimeoutException, InterruptedException {
- LiveData<List<UserAndPet>> liveData = mRawDao.getUserAndPetListObservable(
- "SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
- + " ORDER BY mId ASC, mPetId ASC");
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> liveData.observeForever(user -> {
- })
- );
- drain();
- assertThat(liveData.getValue(), is(emptyList()));
-
- User[] users = TestUtil.createUsersArray(3, 5);
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
- mUserDao.insertAll(users);
- drain();
- List<UserAndPet> justUsers = liveData.getValue();
- //noinspection ConstantConditions
- assertThat(justUsers.size(), is(2));
- assertThat(justUsers.get(0).getUser(), is(users[0]));
- assertThat(justUsers.get(1).getUser(), is(users[1]));
- assertThat(justUsers.get(0).getPet(), is(nullValue()));
- assertThat(justUsers.get(1).getPet(), is(nullValue()));
-
- mPetDao.insertAll(pets);
- drain();
- List<UserAndPet> allItems = liveData.getValue();
- //noinspection ConstantConditions
- assertThat(allItems.size(), is(3));
- // row 0
- assertThat(allItems.get(0).getUser(), is(users[0]));
- assertThat(allItems.get(0).getPet(), is(pets[0]));
- // row 1
- assertThat(allItems.get(1).getUser(), is(users[0]));
- assertThat(allItems.get(1).getPet(), is(pets[1]));
- // row 2
- assertThat(allItems.get(2).getUser(), is(users[1]));
- assertThat(allItems.get(2).getPet(), is(nullValue()));
-
- mDatabase.clearAllTables();
- drain();
- assertThat(liveData.getValue(), is(emptyList()));
- }
-
- @Test
- public void relation_liveData() throws TimeoutException, InterruptedException {
- LiveData<UserAndAllPets> liveData = mRawDao
- .getUserAndAllPetsObservable("SELECT * FROM User WHERE mId = 3");
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> liveData.observeForever(user -> {
- })
- );
- drain();
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- drain();
- assertThat(liveData.getValue().user, is(user));
- assertThat(liveData.getValue().pets, is(emptyList()));
- Pet[] pets = TestUtil.createPetsForUser(3, 1, 5);
- mPetDao.insertAll(pets);
- drain();
- assertThat(liveData.getValue().user, is(user));
- assertThat(liveData.getValue().pets, is(Arrays.asList(pets)));
- }
-
- private void drain() throws TimeoutException, InterruptedException {
- mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RelationWithReservedKeywordTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RelationWithReservedKeywordTest.java
deleted file mode 100644
index b01ed45..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RelationWithReservedKeywordTest.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2018 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.MatcherAssert.assertThat;
-
-import static java.util.Collections.singletonList;
-
-import android.arch.persistence.room.ColumnInfo;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.Embedded;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.ForeignKey;
-import android.arch.persistence.room.Index;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.PrimaryKey;
-import android.arch.persistence.room.Query;
-import android.arch.persistence.room.Relation;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.Transaction;
-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.List;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class RelationWithReservedKeywordTest {
- private MyDatabase mDb;
-
- @Before
- public void initDb() {
- mDb = Room.inMemoryDatabaseBuilder(
- InstrumentationRegistry.getTargetContext(),
- MyDatabase.class).build();
- }
-
- @Test
- public void loadRelation() {
- Category category = new Category(1, "cat1");
- mDb.getDao().insert(category);
- Topic topic = new Topic(2, 1, "foo");
- mDb.getDao().insert(topic);
- List<CategoryWithTopics> categoryWithTopics = mDb.getDao().loadAll();
- assertThat(categoryWithTopics.size(), is(1));
- assertThat(categoryWithTopics.get(0).category, is(category));
- assertThat(categoryWithTopics.get(0).topics, is(singletonList(topic)));
- }
-
- @Entity(tableName = "categories")
- static class Category {
-
- @PrimaryKey(autoGenerate = true)
- public final long id;
-
- public final String name;
-
- Category(long id, String name) {
- this.id = id;
- this.name = name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Category category = (Category) o;
- //noinspection SimplifiableIfStatement
- if (id != category.id) return false;
- return name != null ? name.equals(category.name) : category.name == null;
- }
-
- @Override
- public int hashCode() {
- int result = (int) (id ^ (id >>> 32));
- result = 31 * result + (name != null ? name.hashCode() : 0);
- return result;
- }
- }
-
- @Dao
- interface MyDao {
- @Transaction
- @Query("SELECT * FROM categories")
- List<CategoryWithTopics> loadAll();
-
- @Insert
- void insert(Category... categories);
-
- @Insert
- void insert(Topic... topics);
- }
-
- @Database(
- entities = {Category.class, Topic.class},
- version = 1,
- exportSchema = false)
- abstract static class MyDatabase extends RoomDatabase {
- abstract MyDao getDao();
- }
-
-
- @SuppressWarnings("WeakerAccess")
- static class CategoryWithTopics {
- @Embedded
- public Category category;
-
- @Relation(
- parentColumn = "id",
- entityColumn = "category_id",
- entity = Topic.class)
- public List<Topic> topics;
- }
-
- @Entity(
- tableName = "topics",
- foreignKeys = @ForeignKey(
- entity = Category.class,
- parentColumns = "id",
- childColumns = "category_id",
- onDelete = ForeignKey.CASCADE),
- indices = @Index("category_id"))
- static class Topic {
-
- @PrimaryKey(autoGenerate = true)
- public final long id;
-
- @ColumnInfo(name = "category_id")
- public final long categoryId;
-
- public final String to;
-
- Topic(long id, long categoryId, String to) {
- this.id = id;
- this.categoryId = categoryId;
- this.to = to;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Topic topic = (Topic) o;
- if (id != topic.id) return false;
- //noinspection SimplifiableIfStatement
- if (categoryId != topic.categoryId) return false;
- return to != null ? to.equals(topic.to) : topic.to == null;
- }
-
- @Override
- public int hashCode() {
- int result = (int) (id ^ (id >>> 32));
- result = 31 * result + (int) (categoryId ^ (categoryId >>> 32));
- result = 31 * result + (to != null ? to.hashCode() : 0);
- return result;
- }
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java
deleted file mode 100644
index 01d071e..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * 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.MatcherAssert.assertThat;
-
-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;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-import java.util.Collections;
-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;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class RxJava2Test extends TestDatabaseTest {
-
- private TestScheduler mTestScheduler;
-
- @Before
- public void setupSchedulers() {
- mTestScheduler = new TestScheduler();
- mTestScheduler.start();
- ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- mTestScheduler.scheduleDirect(runnable);
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- Assert.fail("no main thread in this test");
- }
-
- @Override
- public boolean isMainThread() {
- return false;
- }
- });
- }
-
- @After
- public void clearSchedulers() {
- mTestScheduler.shutdown();
- ArchTaskExecutor.getInstance().setDelegate(null);
- }
-
- private void drain() throws InterruptedException {
- mTestScheduler.triggerActions();
- }
-
- @Test
- public void maybeUser_Empty() throws InterruptedException {
- TestObserver<User> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.maybeUserById(3).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- testObserver.assertNoValues();
- disposable.dispose();
- }
-
- @Test
- public void maybeUser_WithData() throws InterruptedException {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- TestObserver<User> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.maybeUserById(3).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- testObserver.assertValue(user);
-
- disposable.dispose();
- }
-
- @Test
- public void maybeUsers_EmptyList() throws InterruptedException {
- TestObserver<List<User>> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.maybeUsersByIds(3, 5, 7).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- testObserver.assertValue(Collections.<User>emptyList());
- disposable.dispose();
- }
-
- @Test
- public void maybeUsers_WithValue() throws InterruptedException {
- User[] users = TestUtil.createUsersArray(3, 5);
- mUserDao.insertAll(users);
- TestObserver<List<User>> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.maybeUsersByIds(3, 5, 7).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- // since this is a clean db, it is ok to rely on the order for the test.
- testObserver.assertValue(Arrays.asList(users));
- disposable.dispose();
- }
-
- @Test
- public void singleUser_Empty() throws InterruptedException {
- TestObserver<User> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.singleUserById(3).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- // figure out which error we should dispatch
- testObserver.assertError(EmptyResultSetException.class);
- testObserver.assertNoValues();
- disposable.dispose();
- }
-
- @Test
- public void singleUser_WithData() throws InterruptedException {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- TestObserver<User> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.singleUserById(3).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- testObserver.assertValue(user);
-
- disposable.dispose();
- }
-
- @Test
- public void singleUsers_EmptyList() throws InterruptedException {
- TestObserver<List<User>> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.singleUsersByIds(3, 5, 7).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- testObserver.assertValue(Collections.<User>emptyList());
- disposable.dispose();
- }
-
- @Test
- public void singleUsers_WithValue() throws InterruptedException {
- User[] users = TestUtil.createUsersArray(3, 5);
- mUserDao.insertAll(users);
- TestObserver<List<User>> testObserver = new TestObserver<>();
- Disposable disposable = mUserDao.singleUsersByIds(3, 5, 7).observeOn(mTestScheduler)
- .subscribeWith(testObserver);
- drain();
- testObserver.assertComplete();
- // since this is a clean db, it is ok to rely on the order for the test.
- testObserver.assertValue(Arrays.asList(users));
- disposable.dispose();
- }
-
- @Test
- public void observeOnce() throws InterruptedException {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- drain();
- TestSubscriber<User> consumer = new TestSubscriber<>();
- Disposable disposable = mUserDao.flowableUserById(3).subscribeWith(consumer);
- drain();
- consumer.assertValue(user);
- disposable.dispose();
- }
-
- @Test
- public void observeChangeAndDispose() throws InterruptedException {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- drain();
- TestSubscriber<User> consumer = new TestSubscriber<>();
- Disposable disposable = mUserDao.flowableUserById(3).observeOn(mTestScheduler)
- .subscribeWith(consumer);
- drain();
- assertThat(consumer.values().get(0), is(user));
- user.setName("rxy");
- mUserDao.insertOrReplace(user);
- drain();
- User next = consumer.values().get(1);
- assertThat(next, is(user));
- disposable.dispose();
- user.setName("foo");
- mUserDao.insertOrReplace(user);
- drain();
- assertThat(consumer.valueCount(), is(2));
- }
-
- @Test
- @MediumTest
- public void observeEmpty() throws InterruptedException {
- TestSubscriber<User> consumer = new TestSubscriber<>();
- Disposable disposable = mUserDao.flowableUserById(3).observeOn(mTestScheduler)
- .subscribeWith(consumer);
- drain();
- consumer.assertNoValues();
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- drain();
- assertThat(consumer.values().get(0), is(user));
- disposable.dispose();
- user.setAge(88);
- mUserDao.insertOrReplace(user);
- drain();
- assertThat(consumer.valueCount(), is(1));
- }
-
- @Test
- public void flowableCountUsers() throws InterruptedException {
- TestSubscriber<Integer> consumer = new TestSubscriber<>();
- mUserDao.flowableCountUsers()
- .observeOn(mTestScheduler)
- .subscribe(consumer);
- drain();
- assertThat(consumer.values().get(0), is(0));
- mUserDao.insertAll(TestUtil.createUsersArray(1, 3, 4, 6));
- drain();
- assertThat(consumer.values().get(1), is(4));
- mUserDao.deleteByUids(3, 7);
- drain();
- assertThat(consumer.values().get(2), is(3));
- mUserDao.deleteByUids(101);
- drain();
- assertThat(consumer.valueCount(), is(3));
- }
-
- @Test
- @MediumTest
- public void publisherCountUsers() throws InterruptedException {
- TestSubscriber<Integer> subscriber = new TestSubscriber<>();
- mUserDao.publisherCountUsers().subscribe(subscriber);
- drain();
- subscriber.assertSubscribed();
- subscriber.request(2);
- drain();
- subscriber.assertValue(0);
- mUserDao.insert(TestUtil.createUser(2));
- drain();
- subscriber.assertValues(0, 1);
- 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java
deleted file mode 100644
index fcd0b00..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
deleted file mode 100644
index 793523c..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ /dev/null
@@ -1,611 +0,0 @@
-/*
- * Copyright (C) 2016 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.equalTo;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.hasSize;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-import android.arch.persistence.room.integration.testapp.dao.BlobEntityDao;
-import android.arch.persistence.room.integration.testapp.dao.PetDao;
-import android.arch.persistence.room.integration.testapp.dao.ProductDao;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
-import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
-import android.arch.persistence.room.integration.testapp.vo.Day;
-import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
-import android.arch.persistence.room.integration.testapp.vo.Pet;
-import android.arch.persistence.room.integration.testapp.vo.Product;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteConstraintException;
-import android.database.sqlite.SQLiteException;
-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.Calendar;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class SimpleEntityReadWriteTest {
- private UserDao mUserDao;
- private BlobEntityDao mBlobEntityDao;
- private PetDao mPetDao;
- private UserPetDao mUserPetDao;
- private ProductDao mProductDao;
-
- @Before
- public void createDb() {
- Context context = InstrumentationRegistry.getTargetContext();
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
- mUserDao = db.getUserDao();
- mPetDao = db.getPetDao();
- mUserPetDao = db.getUserPetDao();
- mBlobEntityDao = db.getBlobEntityDao();
- mProductDao = db.getProductDao();
- }
-
- @Test
- public void writeUserAndReadInList() throws Exception {
- User user = TestUtil.createUser(3);
- user.setName("george");
- mUserDao.insert(user);
- List<User> byName = mUserDao.findUsersByName("george");
- assertThat(byName.get(0), equalTo(user));
- }
-
- @Test
- public void insertNull() throws Exception {
- @SuppressWarnings("ConstantConditions")
- Product product = new Product(1, null);
- Throwable throwable = null;
- try {
- mProductDao.insert(product);
- } catch (Throwable t) {
- throwable = t;
- }
- assertNotNull("Was expecting an exception", throwable);
- assertThat(throwable, instanceOf(SQLiteConstraintException.class));
- }
-
- @Test
- public void insertDifferentEntities() throws Exception {
- User user1 = TestUtil.createUser(3);
- user1.setName("george");
- Pet pet = TestUtil.createPet(1);
- pet.setUserId(3);
- pet.setName("a");
- mUserPetDao.insertUserAndPet(user1, pet);
- assertThat(mUserDao.count(), is(1));
- List<UserAndAllPets> inserted = mUserPetDao.loadAllUsersWithTheirPets();
- assertThat(inserted, hasSize(1));
- assertThat(inserted.get(0).user.getId(), is(3));
- assertThat(inserted.get(0).user.getName(), is(equalTo("george")));
- assertThat(inserted.get(0).pets, hasSize(1));
- assertThat(inserted.get(0).pets.get(0).getPetId(), is(1));
- assertThat(inserted.get(0).pets.get(0).getName(), is("a"));
- assertThat(inserted.get(0).pets.get(0).getUserId(), is(3));
- pet.setName("b");
- mUserPetDao.updateUsersAndPets(new User[]{user1}, new Pet[]{pet});
- List<UserAndAllPets> updated = mUserPetDao.loadAllUsersWithTheirPets();
- assertThat(updated, hasSize(1));
- assertThat(updated.get(0).pets, hasSize(1));
- assertThat(updated.get(0).pets.get(0).getName(), is("b"));
- User user2 = TestUtil.createUser(5);
- user2.setName("chet");
- mUserDao.insert(user2);
- assertThat(mUserDao.count(), is(2));
- mUserPetDao.delete2UsersAndPets(user1, user2, new Pet[]{pet});
- List<UserAndAllPets> deleted = mUserPetDao.loadAllUsersWithTheirPets();
- assertThat(deleted, hasSize(0));
- }
-
- @Test
- public void insertDifferentEntities_transaction() throws Exception {
- Pet pet = TestUtil.createPet(1);
- mPetDao.insertOrReplace(pet);
- assertThat(mPetDao.count(), is(1));
- User user = TestUtil.createUser(3);
- try {
- mUserPetDao.insertUserAndPet(user, pet);
- fail("Exception expected");
- } catch (SQLiteConstraintException ignored) {
- }
- assertThat(mUserDao.count(), is(0));
- assertThat(mPetDao.count(), is(1));
- }
-
- @Test
- public void throwExceptionOnConflict() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
-
- User user2 = TestUtil.createUser(3);
- try {
- mUserDao.insert(user2);
- throw new AssertionError("didn't throw in conflicting insertion");
- } catch (SQLiteException ignored) {
- }
- }
-
- @Test
- public void replaceOnConflict() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
-
- User user2 = TestUtil.createUser(3);
- mUserDao.insertOrReplace(user2);
-
- assertThat(mUserDao.load(3), equalTo(user2));
- assertThat(mUserDao.load(3), not(equalTo(user)));
- }
-
- @Test
- public void updateSimple() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- user.setName("i am an updated name");
- assertThat(mUserDao.update(user), is(1));
- assertThat(mUserDao.load(user.getId()), equalTo(user));
- }
-
- @Test
- public void updateNonExisting() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- User user2 = TestUtil.createUser(4);
- assertThat(mUserDao.update(user2), is(0));
- }
-
- @Test
- public void updateList() {
- List<User> users = TestUtil.createUsersList(3, 4, 5);
- mUserDao.insertAll(users.toArray(new User[3]));
- for (User user : users) {
- user.setName("name " + user.getId());
- }
- assertThat(mUserDao.updateAll(users), is(3));
- for (User user : users) {
- assertThat(mUserDao.load(user.getId()).getName(), is("name " + user.getId()));
- }
- }
-
- @Test
- public void updateListPartial() {
- List<User> existingUsers = TestUtil.createUsersList(3, 4, 5);
- mUserDao.insertAll(existingUsers.toArray(new User[3]));
- for (User user : existingUsers) {
- user.setName("name " + user.getId());
- }
- List<User> allUsers = TestUtil.createUsersList(7, 8, 9);
- allUsers.addAll(existingUsers);
- assertThat(mUserDao.updateAll(allUsers), is(3));
- for (User user : existingUsers) {
- assertThat(mUserDao.load(user.getId()).getName(), is("name " + user.getId()));
- }
- }
-
- @Test
- public void delete() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- assertThat(mUserDao.delete(user), is(1));
- assertThat(mUserDao.delete(user), is(0));
- assertThat(mUserDao.load(3), is(nullValue()));
- }
-
- @Test
- public void deleteAll() {
- User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
- mUserDao.insertAll(users);
- // there is actually no guarantee for this order by works fine since they are ordered for
- // the test and it is a new database (no pages to recycle etc)
- assertThat(mUserDao.loadByIds(3, 5, 7, 9), is(users));
- int deleteCount = mUserDao.deleteAll(new User[]{users[0], users[3],
- TestUtil.createUser(9)});
- assertThat(deleteCount, is(2));
- assertThat(mUserDao.loadByIds(3, 5, 7, 9), is(new User[]{users[1], users[2]}));
- }
-
- @Test
- public void deleteEverything() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- assertThat(mUserDao.count(), is(1));
- int count = mUserDao.deleteEverything();
- assertThat(count, is(1));
- assertThat(mUserDao.count(), is(0));
- }
-
- @Test
- public void findByBoolean() {
- User user1 = TestUtil.createUser(3);
- user1.setAdmin(true);
- User user2 = TestUtil.createUser(5);
- user2.setAdmin(false);
- mUserDao.insert(user1);
- mUserDao.insert(user2);
- assertThat(mUserDao.findByAdmin(true), is(Arrays.asList(user1)));
- assertThat(mUserDao.findByAdmin(false), is(Arrays.asList(user2)));
- }
-
- @Test
- public void returnBoolean() {
- User user1 = TestUtil.createUser(1);
- User user2 = TestUtil.createUser(2);
- user1.setAdmin(true);
- user2.setAdmin(false);
- mUserDao.insert(user1);
- mUserDao.insert(user2);
- assertThat(mUserDao.isAdmin(1), is(true));
- assertThat(mUserDao.isAdmin(2), is(false));
- }
-
- @Test
- public void findByCollateNoCase() {
- User user = TestUtil.createUser(3);
- user.setCustomField("abc");
- mUserDao.insert(user);
- List<User> users = mUserDao.findByCustomField("ABC");
- assertThat(users, hasSize(1));
- assertThat(users.get(0).getId(), is(3));
- }
-
- @Test
- public void deleteByAge() {
- User user1 = TestUtil.createUser(3);
- user1.setAge(30);
- User user2 = TestUtil.createUser(5);
- user2.setAge(45);
- mUserDao.insert(user1);
- mUserDao.insert(user2);
- assertThat(mUserDao.deleteAgeGreaterThan(60), is(0));
- assertThat(mUserDao.deleteAgeGreaterThan(45), is(0));
- assertThat(mUserDao.deleteAgeGreaterThan(35), is(1));
- assertThat(mUserDao.loadByIds(3, 5), is(new User[]{user1}));
- }
-
- @Test
- public void deleteByAgeRange() {
- User user1 = TestUtil.createUser(3);
- user1.setAge(30);
- User user2 = TestUtil.createUser(5);
- user2.setAge(45);
- mUserDao.insert(user1);
- mUserDao.insert(user2);
- assertThat(mUserDao.deleteByAgeRange(35, 40), is(0));
- assertThat(mUserDao.deleteByAgeRange(25, 30), is(1));
- assertThat(mUserDao.loadByIds(3, 5), is(new User[]{user2}));
- }
-
- @Test
- public void deleteByUIds() {
- User[] users = TestUtil.createUsersArray(3, 5, 7, 9, 11);
- mUserDao.insertAll(users);
- assertThat(mUserDao.deleteByUids(2, 4, 6), is(0));
- assertThat(mUserDao.deleteByUids(3, 11), is(2));
- assertThat(mUserDao.loadByIds(3, 5, 7, 9, 11), is(new User[]{
- users[1], users[2], users[3]
- }));
- }
-
- @Test
- public void updateNameById() {
- User[] usersArray = TestUtil.createUsersArray(3, 5, 7);
- mUserDao.insertAll(usersArray);
- assertThat("test sanity", usersArray[1].getName(), not(equalTo("updated name")));
- int changed = mUserDao.updateById(5, "updated name");
- assertThat(changed, is(1));
- assertThat(mUserDao.load(5).getName(), is("updated name"));
- }
-
- @Test
- public void incrementIds() {
- User[] usersArr = TestUtil.createUsersArray(2, 4, 6);
- mUserDao.insertAll(usersArr);
- mUserDao.incrementIds(1);
- assertThat(mUserDao.loadIds(), is(Arrays.asList(3, 5, 7)));
- }
-
- @Test
- public void incrementAgeOfAll() {
- User[] users = TestUtil.createUsersArray(3, 5, 7);
- users[0].setAge(3);
- users[1].setAge(5);
- users[2].setAge(7);
- mUserDao.insertAll(users);
- assertThat(mUserDao.count(), is(3));
- mUserDao.incrementAgeOfAll();
- for (User user : mUserDao.loadByIds(3, 5, 7)) {
- assertThat(user.getAge(), is(user.getId() + 1));
- }
- }
-
- @Test
- public void findByIntQueryParameter() {
- User user = TestUtil.createUser(1);
- final String name = "my name";
- user.setName(name);
- mUserDao.insert(user);
- assertThat(mUserDao.findByNameLength(name.length()), is(Collections.singletonList(user)));
- }
-
- @Test
- public void findByIntFieldMatch() {
- User user = TestUtil.createUser(1);
- user.setAge(19);
- mUserDao.insert(user);
- assertThat(mUserDao.findByAge(19), is(Collections.singletonList(user)));
- }
-
- @Test
- public void customConverterField() {
- User user = TestUtil.createUser(20);
- Date theDate = new Date(System.currentTimeMillis() - 200);
- user.setBirthday(theDate);
- mUserDao.insert(user);
- assertThat(mUserDao.findByBirthdayRange(new Date(theDate.getTime() - 100),
- new Date(theDate.getTime() + 1)).get(0), is(user));
- assertThat(mUserDao.findByBirthdayRange(new Date(theDate.getTime()),
- new Date(theDate.getTime() + 1)).size(), is(0));
- }
-
- @Test
- public void renamedField() {
- User user = TestUtil.createUser(3);
- user.setCustomField("foo laaa");
- mUserDao.insertOrReplace(user);
- User loaded = mUserDao.load(3);
- assertThat(loaded.getCustomField(), is("foo laaa"));
- assertThat(loaded, is(user));
- }
-
- @Test
- public void readViaCursor() {
- User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
- mUserDao.insertAll(users);
- Cursor cursor = mUserDao.findUsersAsCursor(3, 5, 9);
- try {
- assertThat(cursor.getCount(), is(3));
- assertThat(cursor.moveToNext(), is(true));
- assertThat(cursor.getInt(0), is(3));
- assertThat(cursor.moveToNext(), is(true));
- assertThat(cursor.getInt(0), is(5));
- assertThat(cursor.moveToNext(), is(true));
- assertThat(cursor.getInt(0), is(9));
- assertThat(cursor.moveToNext(), is(false));
- } finally {
- cursor.close();
- }
- }
-
- @Test
- public void readDirectWithTypeAdapter() {
- User user = TestUtil.createUser(3);
- user.setBirthday(null);
- mUserDao.insert(user);
- assertThat(mUserDao.getBirthday(3), is(nullValue()));
- Calendar calendar = Calendar.getInstance();
- calendar.add(Calendar.YEAR, 3);
- Date birthday = calendar.getTime();
- user.setBirthday(birthday);
-
- mUserDao.update(user);
- assertThat(mUserDao.getBirthday(3), is(birthday));
- }
-
- @Test
- public void emptyInQuery() {
- User[] users = mUserDao.loadByIds();
- assertThat(users, is(new User[0]));
- }
-
- @Test
- public void blob() {
- BlobEntity a = new BlobEntity(1, "abc".getBytes());
- BlobEntity b = new BlobEntity(2, "def".getBytes());
- mBlobEntityDao.insert(a);
- mBlobEntityDao.insert(b);
- List<BlobEntity> list = mBlobEntityDao.selectAll();
- assertThat(list, hasSize(2));
- mBlobEntityDao.updateContent(2, "ghi".getBytes());
- assertThat(mBlobEntityDao.getContent(2), is(equalTo("ghi".getBytes())));
- }
-
- @Test
- public void transactionByRunnable() {
- User a = TestUtil.createUser(3);
- User b = TestUtil.createUser(5);
- mUserDao.insertBothByRunnable(a, b);
- assertThat(mUserDao.count(), is(2));
- }
-
- @Test
- public void transactionByRunnable_failure() {
- User a = TestUtil.createUser(3);
- User b = TestUtil.createUser(3);
- boolean caught = false;
- try {
- mUserDao.insertBothByRunnable(a, b);
- } catch (SQLiteConstraintException e) {
- caught = true;
- }
- assertTrue("SQLiteConstraintException expected", caught);
- assertThat(mUserDao.count(), is(0));
- }
-
- @Test
- public void transactionByCallable() {
- User a = TestUtil.createUser(3);
- User b = TestUtil.createUser(5);
- int count = mUserDao.insertBothByCallable(a, b);
- assertThat(mUserDao.count(), is(2));
- assertThat(count, is(2));
- }
-
- @Test
- public void transactionByCallable_failure() {
- User a = TestUtil.createUser(3);
- User b = TestUtil.createUser(3);
- boolean caught = false;
- try {
- mUserDao.insertBothByCallable(a, b);
- } catch (SQLiteConstraintException e) {
- caught = true;
- }
- assertTrue("SQLiteConstraintException expected", caught);
- assertThat(mUserDao.count(), is(0));
- }
-
- @Test
- public void transactionByDefaultImplementation() {
- Pet pet1 = TestUtil.createPet(1);
- mPetDao.insertOrReplace(pet1);
- assertThat(mPetDao.count(), is(1));
- assertThat(mPetDao.allIds()[0], is(1));
- Pet pet2 = TestUtil.createPet(2);
- mPetDao.deleteAndInsert(pet1, pet2, false);
- assertThat(mPetDao.count(), is(1));
- assertThat(mPetDao.allIds()[0], is(2));
- }
-
- @Test
- public void transactionByDefaultImplementation_failure() {
- Pet pet1 = TestUtil.createPet(1);
- mPetDao.insertOrReplace(pet1);
- assertThat(mPetDao.count(), is(1));
- assertThat(mPetDao.allIds()[0], is(1));
- Pet pet2 = TestUtil.createPet(2);
- Throwable throwable = null;
- try {
- mPetDao.deleteAndInsert(pet1, pet2, true);
- } catch (Throwable t) {
- throwable = t;
- }
- assertNotNull("Was expecting an exception", throwable);
- assertThat(mPetDao.count(), is(1));
- assertThat(mPetDao.allIds()[0], is(1));
- }
-
- @Test
- public void multipleInParamsFollowedByASingleParam_delete() {
- User user = TestUtil.createUser(3);
- user.setAge(30);
- mUserDao.insert(user);
- assertThat(mUserDao.deleteByAgeAndIds(20, Arrays.asList(3, 5)), is(0));
- assertThat(mUserDao.count(), is(1));
- assertThat(mUserDao.deleteByAgeAndIds(30, Arrays.asList(3, 5)), is(1));
- assertThat(mUserDao.count(), is(0));
- }
-
- @Test
- public void multipleInParamsFollowedByASingleParam_update() {
- User user = TestUtil.createUser(3);
- user.setAge(30);
- user.setWeight(10f);
- mUserDao.insert(user);
- assertThat(mUserDao.updateByAgeAndIds(3f, 20, Arrays.asList(3, 5)), is(0));
- assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(10f));
- 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));
- }
-
- @Test
- public void tablePrefix_simpleSelect() {
- User user = TestUtil.createUser(3);
- mUserDao.insert(user);
- NameAndLastName result = mUserDao.getNameAndLastName(3);
- assertThat(result.getName(), is(user.getName()));
- assertThat(result.getLastName(), is(user.getLastName()));
- }
-
- @Test
- public void enumSet_simpleLoad() {
- User a = TestUtil.createUser(3);
- Set<Day> expected = toSet(Day.MONDAY, Day.TUESDAY);
- a.setWorkDays(expected);
- mUserDao.insert(a);
- User loaded = mUserDao.load(3);
- assertThat(loaded.getWorkDays(), is(expected));
- }
-
- @Test
- public void enumSet_query() {
- User user1 = TestUtil.createUser(3);
- user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
- User user2 = TestUtil.createUser(5);
- user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
- mUserDao.insert(user1);
- mUserDao.insert(user2);
- List<User> empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
- assertThat(empty.size(), is(0));
- List<User> friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
- assertThat(friday, is(Arrays.asList(user1)));
- List<User> monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
- assertThat(monday, is(Arrays.asList(user1, user2)));
-
- }
-
- private Set<Day> toSet(Day... days) {
- return new HashSet<>(Arrays.asList(days));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
deleted file mode 100644
index e2525c4..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.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.RawDao;
-import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
-import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
-import android.arch.persistence.room.integration.testapp.dao.ToyDao;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
-import android.arch.persistence.room.integration.testapp.dao.WithClauseDao;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-
-import org.junit.Before;
-
-@SuppressWarnings("WeakerAccess")
-public abstract class TestDatabaseTest {
- protected TestDatabase mDatabase;
- protected UserDao mUserDao;
- protected PetDao mPetDao;
- protected UserPetDao mUserPetDao;
- protected SchoolDao mSchoolDao;
- protected PetCoupleDao mPetCoupleDao;
- protected ToyDao mToyDao;
- protected SpecificDogDao mSpecificDogDao;
- protected WithClauseDao mWithClauseDao;
- protected FunnyNamedDao mFunnyNamedDao;
- protected RawDao mRawDao;
-
- @Before
- public void createDb() {
- Context context = InstrumentationRegistry.getTargetContext();
- mDatabase = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
- mUserDao = mDatabase.getUserDao();
- mPetDao = mDatabase.getPetDao();
- mUserPetDao = mDatabase.getUserPetDao();
- mSchoolDao = mDatabase.getSchoolDao();
- mPetCoupleDao = mDatabase.getPetCoupleDao();
- mToyDao = mDatabase.getToyDao();
- mSpecificDogDao = mDatabase.getSpecificDogDao();
- mWithClauseDao = mDatabase.getWithClauseDao();
- mFunnyNamedDao = mDatabase.getFunnyNamedDao();
- mRawDao = mDatabase.getRawDao();
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestUtil.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestUtil.java
deleted file mode 100644
index d5309b3..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/TestUtil.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2016 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.persistence.room.integration.testapp.vo.Address;
-import android.arch.persistence.room.integration.testapp.vo.Coordinates;
-import android.arch.persistence.room.integration.testapp.vo.Pet;
-import android.arch.persistence.room.integration.testapp.vo.School;
-import android.arch.persistence.room.integration.testapp.vo.Toy;
-import android.arch.persistence.room.integration.testapp.vo.User;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
-
-public class TestUtil {
- public static User[] createUsersArray(int... ids) {
- User[] result = new User[ids.length];
- for (int i = 0; i < ids.length; i++) {
- result[i] = createUser(ids[i]);
- }
- return result;
- }
-
- public static List<User> createUsersList(int... ids) {
- List<User> result = new ArrayList<>();
- for (int id : ids) {
- result.add(createUser(id));
- }
- return result;
- }
-
- public static User createUser(int id) {
- User user = new User();
- user.setId(id);
- user.setName(UUID.randomUUID().toString());
- user.setLastName(UUID.randomUUID().toString());
- user.setAge((int) (10 + Math.random() * 50));
- user.setCustomField(UUID.randomUUID().toString());
- user.setBirthday(new Date());
- return user;
- }
-
- public static Pet createPet(int id) {
- Pet pet = new Pet();
- pet.setPetId(id);
- pet.setName(UUID.randomUUID().toString());
- pet.setAdoptionDate(new Date());
- return pet;
- }
-
- public static Toy createToyForPet(Pet pet, int toyId) {
- Toy toy = new Toy();
- toy.setName("toy " + toyId);
- toy.setId(toyId);
- toy.setPetId(pet.getPetId());
- return toy;
- }
-
- public static Pet[] createPetsForUser(int uid, int petStartId, int count) {
- Pet[] pets = new Pet[count];
- for (int i = 0; i < count; i++) {
- Pet pet = createPet(petStartId++);
- pet.setUserId(uid);
- pets[i] = pet;
- }
- return pets;
- }
-
- public static School createSchool(int id, int managerId) {
- School school = new School();
- school.setId(id);
- school.setName(UUID.randomUUID().toString());
- school.setManager(createUser(managerId));
- school.setAddress(createAddress());
- return school;
- }
-
- private static Address createAddress() {
- Address address = new Address();
- address.setCoordinates(createCoordinates());
- address.setPostCode((int) (Math.random() * 1000 + 1000));
- address.setState(UUID.randomUUID().toString().substring(0, 2));
- address.setStreet(UUID.randomUUID().toString());
- return address;
- }
-
- private static Coordinates createCoordinates() {
- Coordinates coordinates = new Coordinates();
- coordinates.lat = Math.random();
- coordinates.lng = Math.random();
- return coordinates;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java
deleted file mode 100644
index 9209638..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.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;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-public class WithClauseTest extends TestDatabaseTest{
- @Test
- public void noSourceOfData() {
- @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
- List<Integer> expected = Arrays.asList(1);
- List<Integer> actual = mWithClauseDao.getFactorials(0);
- assertThat(expected, is(actual));
-
- expected = Arrays.asList(1, 1, 2, 6, 24);
- actual = mWithClauseDao.getFactorials(4);
- assertThat(expected, is(actual));
- }
-
- @Test
- public void sourceOfData() {
- List<String> expected = new ArrayList<>();
- List<String> actual = mWithClauseDao.getUsersWithFactorialIds(0);
- assertThat(expected, is(actual));
-
- User user = new User();
- user.setId(0);
- user.setName("Zero");
- mUserDao.insert(user);
- actual = mWithClauseDao.getUsersWithFactorialIds(0);
- assertThat(expected, is(actual));
-
- user = new User();
- user.setId(1);
- user.setName("One");
- mUserDao.insert(user);
- expected.add("One");
- actual = mWithClauseDao.getUsersWithFactorialIds(0);
- assertThat(expected, is(actual));
-
- user = new User();
- user.setId(6);
- user.setName("Six");
- mUserDao.insert(user);
- actual = mWithClauseDao.getUsersWithFactorialIds(0);
- assertThat(expected, is(actual));
-
- actual = mWithClauseDao.getUsersWithFactorialIds(3);
- expected.add("Six");
- assertThat(expected, is(actual));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java
deleted file mode 100644
index 5bc01ce..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/WriteAheadLoggingTest.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright 2018 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.equalTo;
-import static org.hamcrest.CoreMatchers.hasItem;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalToIgnoringCase;
-import static org.hamcrest.Matchers.hasSize;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-
-import android.arch.lifecycle.LiveData;
-import android.arch.lifecycle.Observer;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.room.InvalidationTracker;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-import android.arch.persistence.room.integration.testapp.dao.UserDao;
-import android.arch.persistence.room.integration.testapp.vo.User;
-import android.content.Context;
-import android.database.Cursor;
-import android.support.annotation.NonNull;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-@SdkSuppress(minSdkVersion = 16)
-public class WriteAheadLoggingTest {
-
- private static final String DATABASE_NAME = "wal.db";
- private TestDatabase mDatabase;
-
- @Before
- public void openDatabase() {
- Context context = InstrumentationRegistry.getTargetContext();
- context.deleteDatabase(DATABASE_NAME);
- mDatabase = Room.databaseBuilder(context, TestDatabase.class, DATABASE_NAME)
- .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
- .build();
- }
-
- @After
- public void closeDatabase() {
- mDatabase.close();
- Context context = InstrumentationRegistry.getTargetContext();
- context.deleteDatabase(DATABASE_NAME);
- }
-
- @Test
- public void checkJournalMode() {
- Cursor c = null;
- try {
- SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
- c = db.query("PRAGMA journal_mode");
- c.moveToFirst();
- String journalMode = c.getString(0);
- assertThat(journalMode, is(equalToIgnoringCase("wal")));
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- @Test
- public void disableWal() {
- Context context = InstrumentationRegistry.getTargetContext();
- mDatabase.close();
- mDatabase = Room.databaseBuilder(context, TestDatabase.class, DATABASE_NAME)
- .setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
- .build();
- Cursor c = null;
- try {
- SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
- c = db.query("PRAGMA journal_mode");
- c.moveToFirst();
- String journalMode = c.getString(0);
- assertThat(journalMode, is(not(equalToIgnoringCase("wal"))));
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- @Test
- public void observeLiveData() {
- UserDao dao = mDatabase.getUserDao();
- LiveData<User> user1 = dao.liveUserById(1);
- Observer<User> observer = startObserver(user1);
- dao.insert(TestUtil.createUser(1));
- verify(observer, timeout(3000).atLeastOnce())
- .onChanged(argThat(user -> user != null && user.getId() == 1));
- stopObserver(user1, observer);
- }
-
- @Test
- public void observeLiveDataWithTransaction() {
- UserDao dao = mDatabase.getUserDao();
- LiveData<User> user1 = dao.liveUserByIdInTransaction(1);
- Observer<User> observer = startObserver(user1);
- dao.insert(TestUtil.createUser(1));
- verify(observer, timeout(3000).atLeastOnce())
- .onChanged(argThat(user -> user != null && user.getId() == 1));
- stopObserver(user1, observer);
- }
-
- @Test
- public void parallelWrites() throws InterruptedException, ExecutionException {
- int numberOfThreads = 10;
- ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
- ArrayList<Future<Boolean>> futures = new ArrayList<>();
- for (int i = 0; i < numberOfThreads; i++) {
- final int id = i + 1;
- futures.add(i, executor.submit(() -> {
- User user = TestUtil.createUser(id);
- user.setName("user" + id);
- mDatabase.getUserDao().insert(user);
- return true;
- }));
- }
- LiveData<List<User>> usersList = mDatabase.getUserDao().liveUsersListByName("user");
- Observer<List<User>> observer = startObserver(usersList);
- for (Future future : futures) {
- assertThat(future.get(), is(true));
- }
- verify(observer, timeout(3000).atLeastOnce())
- .onChanged(argThat(users -> users != null && users.size() == numberOfThreads));
- stopObserver(usersList, observer);
- }
-
- @Test
- public void readInBackground() throws InterruptedException, ExecutionException {
- final UserDao dao = mDatabase.getUserDao();
- final User user1 = TestUtil.createUser(1);
- dao.insert(user1);
- try {
- mDatabase.beginTransaction();
- dao.delete(user1);
- ExecutorService executor = Executors.newSingleThreadExecutor();
- Future<?> future = executor.submit(() ->
- assertThat(dao.load(1), is(equalTo(user1))));
- future.get();
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- assertThat(dao.count(), is(0));
- }
-
- @Test
- @LargeTest
- public void observeInvalidationInBackground() throws InterruptedException, ExecutionException {
- final UserDao dao = mDatabase.getUserDao();
- final User user1 = TestUtil.createUser(1);
- final CountDownLatch observerRegistered = new CountDownLatch(1);
- final CountDownLatch onInvalidatedCalled = new CountDownLatch(1);
- dao.insert(user1);
- Future future;
- try {
- mDatabase.beginTransaction();
- dao.delete(user1);
- future = Executors.newSingleThreadExecutor().submit(() -> {
- // Adding this observer will be blocked by the surrounding transaction.
- mDatabase.getInvalidationTracker().addObserver(
- new InvalidationTracker.Observer("User") {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- onInvalidatedCalled.countDown(); // This should not happen
- }
- });
- observerRegistered.countDown();
- });
- mDatabase.setTransactionSuccessful();
- } finally {
- assertThat(observerRegistered.getCount(), is(1L));
- mDatabase.endTransaction();
- }
- assertThat(dao.count(), is(0));
- assertThat(observerRegistered.await(3000, TimeUnit.MILLISECONDS), is(true));
- future.get();
- assertThat(onInvalidatedCalled.await(500, TimeUnit.MILLISECONDS), is(false));
- }
-
- @Test
- public void invalidation() throws InterruptedException {
- final CountDownLatch latch = new CountDownLatch(1);
- mDatabase.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("User") {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- assertThat(tables, hasSize(1));
- assertThat(tables, hasItem("User"));
- latch.countDown();
- }
- });
- mDatabase.getUserDao().insert(TestUtil.createUser(1));
- assertThat(latch.await(3000, TimeUnit.MILLISECONDS), is(true));
- }
-
- private static <T> Observer<T> startObserver(LiveData<T> liveData) {
- @SuppressWarnings("unchecked")
- Observer<T> observer = mock(Observer.class);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
- liveData.observeForever(observer));
- return observer;
- }
-
- private static <T> void stopObserver(LiveData<T> liveData, Observer<T> observer) {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
- liveData.removeObserver(observer));
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Address.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Address.java
deleted file mode 100644
index 46f3bb6..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Address.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.Embedded;
-
-public class Address {
- @ColumnInfo(name = "street")
- private String mStreet;
- @ColumnInfo(name = "state")
- private String mState;
- @ColumnInfo(name = "post_code")
- private int mPostCode;
- @Embedded
- private Coordinates mCoordinates;
-
- public String getStreet() {
- return mStreet;
- }
-
- public void setStreet(String street) {
- mStreet = street;
- }
-
- public String getState() {
- return mState;
- }
-
- public void setState(String state) {
- mState = state;
- }
-
- public int getPostCode() {
- return mPostCode;
- }
-
- public void setPostCode(int postCode) {
- mPostCode = postCode;
- }
-
- public Coordinates getCoordinates() {
- return mCoordinates;
- }
-
- public void setCoordinates(Coordinates coordinates) {
- mCoordinates = coordinates;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Address address = (Address) o;
-
- if (mPostCode != address.mPostCode) return false;
- if (mStreet != null ? !mStreet.equals(address.mStreet) : address.mStreet != null) {
- return false;
- }
- if (mState != null ? !mState.equals(address.mState) : address.mState != null) {
- return false;
- }
- return mCoordinates != null ? mCoordinates.equals(address.mCoordinates)
- : address.mCoordinates == null;
- }
-
- @Override
- public int hashCode() {
- int result = mStreet != null ? mStreet.hashCode() : 0;
- result = 31 * result + (mState != null ? mState.hashCode() : 0);
- result = 31 * result + mPostCode;
- result = 31 * result + (mCoordinates != null ? mCoordinates.hashCode() : 0);
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/AvgWeightByAge.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/AvgWeightByAge.java
deleted file mode 100644
index 4d22f13..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/AvgWeightByAge.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.Ignore;
-
-@SuppressWarnings("unused")
-public class AvgWeightByAge {
-
- private int mAge;
-
- @ColumnInfo(name = "AVG(mWeight)")
- private float mWeight;
-
- public AvgWeightByAge() {
- }
-
- @Ignore
- public AvgWeightByAge(int age, float weight) {
- mAge = age;
- mWeight = weight;
- }
-
- public int getAge() {
- return mAge;
- }
-
- public void setAge(int age) {
- mAge = age;
- }
-
- public float getWeight() {
- return mWeight;
- }
-
- public void setWeight(float weight) {
- mWeight = weight;
- }
-
- @Override
- public String toString() {
- return "AvgWeightByAge{"
- + "mAge=" + mAge
- + ", mWeight=" + mWeight
- + '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- AvgWeightByAge that = (AvgWeightByAge) o;
-
- //noinspection SimplifiableIfStatement
- if (mAge != that.mAge) {
- return false;
- }
- return Float.compare(that.mWeight, mWeight) == 0;
- }
-
- @Override
- public int hashCode() {
- int result = mAge;
- result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/BlobEntity.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/BlobEntity.java
deleted file mode 100644
index 134afc7..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/BlobEntity.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 BlobEntity {
- @PrimaryKey
- public long id;
- public byte[] content;
-
- public BlobEntity(long id, byte[] content) {
- this.id = id;
- this.content = content;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Coordinates.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Coordinates.java
deleted file mode 100644
index e8cfd06..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Coordinates.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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;
-
-public class Coordinates {
- public double lat;
- public double lng;
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Coordinates that = (Coordinates) o;
-
- if (Double.compare(that.lat, lat) != 0) return false;
- return Double.compare(that.lng, lng) == 0;
- }
-
- @Override
- public int hashCode() {
- int result;
- long temp;
- temp = Double.doubleToLongBits(lat);
- result = (int) (temp ^ (temp >>> 32));
- temp = Double.doubleToLongBits(lng);
- result = 31 * result + (int) (temp ^ (temp >>> 32));
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java
deleted file mode 100644
index e02b91c..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 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;
-
-public enum Day {
- MONDAY,
- TUESDAY,
- WEDNESDAY,
- THURSDAY,
- FRIDAY,
- SATURDAY,
- SUNDAY
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/EmbeddedUserAndAllPets.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/EmbeddedUserAndAllPets.java
deleted file mode 100644
index ca82bfe..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/EmbeddedUserAndAllPets.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.Embedded;
-
-public class EmbeddedUserAndAllPets {
- @Embedded
- UserAndAllPets mUserAndAllPets;
-
- public UserAndAllPets getUserAndAllPets() {
- return mUserAndAllPets;
- }
-
- public void setUserAndAllPets(UserAndAllPets userAndAllPets) {
- this.mUserAndAllPets = userAndAllPets;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java
deleted file mode 100644
index 20f3c21..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntAutoIncPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntAutoIncPKeyEntity.java
deleted file mode 100644
index 3392649..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntAutoIncPKeyEntity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 IntAutoIncPKeyEntity {
- @PrimaryKey(autoGenerate = true)
- public int pKey;
- public String data;
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntegerAutoIncPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntegerAutoIncPKeyEntity.java
deleted file mode 100644
index 636ffd9..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntegerAutoIncPKeyEntity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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 IntegerAutoIncPKeyEntity {
- @PrimaryKey(autoGenerate = true)
- public Integer pKey;
- public String data;
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java
deleted file mode 100644
index cae1843..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
deleted file mode 100644
index a6e8223..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 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;
-
-
-public class NameAndLastName {
- private String mName;
- private String mLastName;
-
- public NameAndLastName(String name, String lastName) {
- mName = name;
- mLastName = lastName;
- }
-
- public String getName() {
- return mName;
- }
-
- public String getLastName() {
- return mLastName;
- }
-
- @SuppressWarnings("SimplifiableIfStatement")
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- NameAndLastName that = (NameAndLastName) o;
-
- if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
- return mLastName != null ? mLastName.equals(that.mLastName) : that.mLastName == null;
- }
-
- @Override
- public int hashCode() {
- int result = mName != null ? mName.hashCode() : 0;
- result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java
deleted file mode 100644
index 895a35a..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Pet.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Pet.java
deleted file mode 100644
index cc7549f..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Pet.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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;
-
-import java.util.Date;
-
-@Entity
-public class Pet {
- @PrimaryKey
- private int mPetId;
- private int mUserId;
- @ColumnInfo(name = "mPetName")
- private String mName;
-
- private Date mAdoptionDate;
-
- public int getPetId() {
- return mPetId;
- }
-
- public void setPetId(int petId) {
- mPetId = petId;
- }
-
- public String getName() {
- return mName;
- }
-
- public void setName(String name) {
- mName = name;
- }
-
- public int getUserId() {
- return mUserId;
- }
-
- public void setUserId(int userId) {
- mUserId = userId;
- }
-
- public Date getAdoptionDate() {
- return mAdoptionDate;
- }
-
- public void setAdoptionDate(Date adoptionDate) {
- mAdoptionDate = adoptionDate;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Pet pet = (Pet) o;
-
- if (mPetId != pet.mPetId) return false;
- if (mUserId != pet.mUserId) return false;
- if (mName != null ? !mName.equals(pet.mName) : pet.mName != null) return false;
- return mAdoptionDate != null ? mAdoptionDate.equals(pet.mAdoptionDate)
- : pet.mAdoptionDate == null;
- }
-
- @Override
- public int hashCode() {
- int result = mPetId;
- result = 31 * result + mUserId;
- result = 31 * result + (mName != null ? mName.hashCode() : 0);
- result = 31 * result + (mAdoptionDate != null ? mAdoptionDate.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return "Pet{"
- + "mPetId=" + mPetId
- + ", mUserId=" + mUserId
- + ", mName='" + mName + '\''
- + ", mAdoptionDate=" + mAdoptionDate
- + '}';
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetAndToys.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetAndToys.java
deleted file mode 100644
index 69fb591..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetAndToys.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.Embedded;
-import android.arch.persistence.room.Relation;
-
-import java.util.List;
-
-public class PetAndToys {
- @Embedded
- public Pet pet;
- @Relation(parentColumn = "mPetId", entityColumn = "mPetId")
- public List<Toy> toys;
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetCouple.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetCouple.java
deleted file mode 100644
index e5208ed..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetCouple.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.Embedded;
-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;
- @Embedded(prefix = "female_")
- private Pet mFemale;
-
- public Pet getFemale() {
- return mFemale;
- }
-
- public void setFemale(Pet female) {
- mFemale = female;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- PetCouple petCouple = (PetCouple) o;
-
- if (male != null ? !male.equals(petCouple.male) : petCouple.male != null) return false;
- return mFemale != null ? mFemale.equals(petCouple.mFemale) : petCouple.mFemale == null;
- }
-
- @Override
- public int hashCode() {
- int result = male != null ? male.hashCode() : 0;
- result = 31 * result + (mFemale != null ? mFemale.hashCode() : 0);
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java
deleted file mode 100644
index 005afb1..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 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.Embedded;
-import android.arch.persistence.room.Ignore;
-import android.arch.persistence.room.Relation;
-
-import java.util.List;
-
-public class PetWithToyIds {
- @Embedded
- public final Pet pet;
- @Relation(parentColumn = "mPetId", entityColumn = "mPetId", projection = "mId",
- entity = Toy.class)
- public List<Integer> toyIds;
-
- // for the relation
- public PetWithToyIds(Pet pet) {
- this.pet = pet;
- }
-
- @Ignore
- public PetWithToyIds(Pet pet, List<Integer> toyIds) {
- this.pet = pet;
- this.toyIds = toyIds;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- PetWithToyIds that = (PetWithToyIds) o;
-
- if (pet != null ? !pet.equals(that.pet) : that.pet != null) return false;
- return toyIds != null ? toyIds.equals(that.toyIds) : that.toyIds == null;
- }
-
- @Override
- public int hashCode() {
- int result = pet != null ? pet.hashCode() : 0;
- result = 31 * result + (toyIds != null ? toyIds.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return "PetWithToyIds{"
- + "pet=" + pet
- + ", toyIds=" + toyIds
- + '}';
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetsToys.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetsToys.java
deleted file mode 100644
index 16bddd6..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/PetsToys.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.Relation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PetsToys {
- public int petId;
- @Relation(parentColumn = "petId", entityColumn = "mPetId")
- public List<Toy> toys;
-
- public PetsToys() {
- toys = new ArrayList<>();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- PetsToys petsToys = (PetsToys) o;
-
- if (petId != petsToys.petId) {
- return false;
- }
- return toys != null ? toys.equals(petsToys.toys) : petsToys.toys == null;
- }
-
- @Override
- public int hashCode() {
- int result = petId;
- result = 31 * result + (toys != null ? toys.hashCode() : 0);
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Product.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Product.java
deleted file mode 100644
index a395aea..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Product.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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(tableName = "products")
-public class Product {
-
- @PrimaryKey(autoGenerate = true)
- public final int id;
-
- @NonNull
- public final String name;
-
- public Product(int id, @NonNull String name) {
- this.id = id;
- this.name = name;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/School.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/School.java
deleted file mode 100644
index f0426f1..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/School.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.Embedded;
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.PrimaryKey;
-
-@Entity
-public class School {
- @PrimaryKey
- private int mId;
- private String mName;
- @Embedded(prefix = "address_")
- public Address address;
-
- @Embedded(prefix = "manager_")
- private User mManager;
-
- public int getId() {
- return mId;
- }
-
- public void setId(int id) {
- mId = id;
- }
-
- public Address getAddress() {
- return address;
- }
-
- public void setAddress(Address address) {
- this.address = address;
- }
-
- public User getManager() {
- return mManager;
- }
-
- public void setManager(User manager) {
- mManager = manager;
- }
-
- public String getName() {
- return mName;
- }
-
- public void setName(String name) {
- mName = name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- School school = (School) o;
-
- if (mId != school.mId) {
- return false;
- }
- if (mName != null ? !mName.equals(school.mName) : school.mName != null) {
- return false;
- }
- if (address != null ? !address.equals(school.address) : school.address != null) {
- return false;
- }
- return mManager != null ? mManager.equals(school.mManager) : school.mManager == null;
- }
-
- @Override
- public int hashCode() {
- int result = mId;
- result = 31 * result + (mName != null ? mName.hashCode() : 0);
- result = 31 * result + (address != null ? address.hashCode() : 0);
- result = 31 * result + (mManager != null ? mManager.hashCode() : 0);
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/SchoolRef.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/SchoolRef.java
deleted file mode 100644
index bbf0cb4..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/SchoolRef.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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;
-
-public class SchoolRef extends School {
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Toy.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Toy.java
deleted file mode 100644
index 5198c2d..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Toy.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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;
-
-/**
- * The toys of a pet.
- */
-@Entity
-public class Toy {
- @PrimaryKey(autoGenerate = true)
- private int mId;
- private String mName;
- private int mPetId;
-
- public int getId() {
- return mId;
- }
-
- public void setId(int id) {
- mId = id;
- }
-
- public String getName() {
- return mName;
- }
-
- public void setName(String name) {
- mName = name;
- }
-
- public int getPetId() {
- return mPetId;
- }
-
- public void setPetId(int petId) {
- mPetId = petId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Toy toy = (Toy) o;
-
- if (mId != toy.mId) return false;
- if (mPetId != toy.mPetId) return false;
- return mName != null ? mName.equals(toy.mName) : toy.mName == null;
- }
-
- @Override
- public int hashCode() {
- int result = mId;
- result = 31 * result + (mName != null ? mName.hashCode() : 0);
- result = 31 * result + mPetId;
- return result;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
deleted file mode 100644
index a615819..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-import android.arch.persistence.room.TypeConverters;
-import android.arch.persistence.room.integration.testapp.TestDatabase;
-
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Set;
-
-@Entity
-@TypeConverters({TestDatabase.Converters.class})
-public class User {
-
- @PrimaryKey
- private int mId;
-
- private String mName;
-
- private String mLastName;
-
- private int mAge;
-
- private boolean mAdmin;
-
- private float mWeight;
-
- private Date mBirthday;
-
- @ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE)
- private String mCustomField;
-
- // bit flags
- private Set<Day> mWorkDays = new HashSet<>();
-
- public int getId() {
- return mId;
- }
-
- public void setId(int id) {
- this.mId = id;
- }
-
- public String getName() {
- return mName;
- }
-
- public void setName(String name) {
- this.mName = name;
- }
-
- public String getLastName() {
- return mLastName;
- }
-
- public void setLastName(String lastName) {
- this.mLastName = lastName;
- }
-
- public int getAge() {
- return mAge;
- }
-
- public void setAge(int age) {
- this.mAge = age;
- }
-
- public boolean isAdmin() {
- return mAdmin;
- }
-
- public void setAdmin(boolean admin) {
- mAdmin = admin;
- }
-
- public float getWeight() {
- return mWeight;
- }
-
- public void setWeight(float weight) {
- mWeight = weight;
- }
-
- public Date getBirthday() {
- return mBirthday;
- }
-
- public void setBirthday(Date birthday) {
- mBirthday = birthday;
- }
-
- public String getCustomField() {
- return mCustomField;
- }
-
- public void setCustomField(String customField) {
- mCustomField = customField;
- }
-
- public Set<Day> getWorkDays() {
- return mWorkDays;
- }
-
- public void setWorkDays(
- Set<Day> workDays) {
- mWorkDays = workDays;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- User user = (User) o;
-
- if (mId != user.mId) return false;
- if (mAge != user.mAge) return false;
- if (mAdmin != user.mAdmin) return false;
- if (Float.compare(user.mWeight, mWeight) != 0) return false;
- if (mName != null ? !mName.equals(user.mName) : user.mName != null) return false;
- if (mLastName != null ? !mLastName.equals(user.mLastName) : user.mLastName != null) {
- return false;
- }
- if (mBirthday != null ? !mBirthday.equals(user.mBirthday) : user.mBirthday != null) {
- return false;
- }
- if (mCustomField != null ? !mCustomField.equals(user.mCustomField)
- : user.mCustomField != null) {
- return false;
- }
- return mWorkDays != null ? mWorkDays.equals(user.mWorkDays) : user.mWorkDays == null;
- }
-
- @Override
- public int hashCode() {
- int result = mId;
- result = 31 * result + (mName != null ? mName.hashCode() : 0);
- result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
- result = 31 * result + mAge;
- result = 31 * result + (mAdmin ? 1 : 0);
- result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
- result = 31 * result + (mBirthday != null ? mBirthday.hashCode() : 0);
- result = 31 * result + (mCustomField != null ? mCustomField.hashCode() : 0);
- result = 31 * result + (mWorkDays != null ? mWorkDays.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return "User{"
- + "mId=" + mId
- + ", mName='" + mName + '\''
- + ", mLastName='" + mLastName + '\''
- + ", mAge=" + mAge
- + ", mAdmin=" + mAdmin
- + ", mWeight=" + mWeight
- + ", mBirthday=" + mBirthday
- + ", mCustomField='" + mCustomField + '\''
- + ", mWorkDays=" + mWorkDays
- + '}';
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndAllPets.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndAllPets.java
deleted file mode 100644
index 24a0710..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndAllPets.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.Embedded;
-import android.arch.persistence.room.Relation;
-
-import java.util.List;
-
-public class UserAndAllPets {
- @Embedded
- public User user;
- @Relation(parentColumn = "mId", entityColumn = "mUserId")
- public List<Pet> pets;
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPet.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPet.java
deleted file mode 100644
index 628e9bf..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPet.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.Embedded;
-
-public class UserAndPet {
- @Embedded
- private User mUser;
- @Embedded
- private Pet mPet;
-
- public User getUser() {
- return mUser;
- }
-
- public void setUser(User user) {
- mUser = user;
- }
-
- public Pet getPet() {
- return mPet;
- }
-
- public void setPet(Pet pet) {
- mPet = pet;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java
deleted file mode 100644
index 28d2495..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 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.Embedded;
-import android.arch.persistence.room.Ignore;
-import android.arch.persistence.room.Relation;
-
-import java.util.Date;
-import java.util.List;
-
-public class UserAndPetAdoptionDates {
- @Embedded
- public final User user;
- @Relation(entity = Pet.class,
- parentColumn = "mId",
- entityColumn = "mUserId",
- projection = "mAdoptionDate")
- public List<Date> petAdoptionDates;
-
- public UserAndPetAdoptionDates(User user) {
- this.user = user;
- }
-
- @Ignore
- public UserAndPetAdoptionDates(User user, List<Date> petAdoptionDates) {
- this.user = user;
- this.petAdoptionDates = petAdoptionDates;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- UserAndPetAdoptionDates that = (UserAndPetAdoptionDates) o;
-
- if (user != null ? !user.equals(that.user) : that.user != null) return false;
- return petAdoptionDates != null ? petAdoptionDates.equals(that.petAdoptionDates)
- : that.petAdoptionDates == null;
- }
-
- @Override
- public int hashCode() {
- int result = user != null ? user.hashCode() : 0;
- result = 31 * result + (petAdoptionDates != null ? petAdoptionDates.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return "UserAndPetAdoptionDates{"
- + "user=" + user
- + ", petAdoptionDates=" + petAdoptionDates
- + '}';
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPetNonNull.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPetNonNull.java
deleted file mode 100644
index 8739bd0..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserAndPetNonNull.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.Embedded;
-import android.support.annotation.NonNull;
-
-public class UserAndPetNonNull {
- @Embedded
- private User mUser;
- @Embedded
- @NonNull
- private Pet mPet;
-
- public User getUser() {
- return mUser;
- }
-
- public void setUser(User user) {
- mUser = user;
- }
-
- public Pet getPet() {
- return mPet;
- }
-
- public void setPet(Pet pet) {
- mPet = pet;
- }
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserIdAndPetNames.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserIdAndPetNames.java
deleted file mode 100644
index 444431e..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserIdAndPetNames.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.Relation;
-
-import java.util.List;
-
-/**
- * Same as Pet class but only keeps name and user id
- */
-public class UserIdAndPetNames {
- @ColumnInfo(name = "mId")
- public int userId;
- @Relation(entity = Pet.class, parentColumn = "mId", entityColumn = "mUserId",
- projection = "mPetName")
- public List<String> names;
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserWithPetsAndToys.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserWithPetsAndToys.java
deleted file mode 100644
index 01c9bed..0000000
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/UserWithPetsAndToys.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.Embedded;
-import android.arch.persistence.room.Relation;
-
-import java.util.List;
-
-public class UserWithPetsAndToys {
- @Embedded
- public User user;
- @Relation(entity = Pet.class, parentColumn = "mId", entityColumn = "mUserId")
- public List<PetAndToys> pets;
-}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
new file mode 100644
index 0000000..2e52952
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/InvalidationTrackerTrojan.java
@@ -0,0 +1,26 @@
+/*
+ * 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 androidx.room;
+
+/**
+ * Trojan class to be able to assert internal state.
+ */
+public class InvalidationTrackerTrojan {
+ public static int countObservers(InvalidationTracker tracker) {
+ return tracker.mObserverMap.size();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/PKeyTestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/PKeyTestDatabase.java
new file mode 100644
index 0000000..233f270
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/PKeyTestDatabase.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 androidx.room.integration.testapp;
+
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.vo.IntAutoIncPKeyEntity;
+import androidx.room.integration.testapp.vo.IntegerAutoIncPKeyEntity;
+import androidx.room.integration.testapp.vo.IntegerPKeyEntity;
+import androidx.room.integration.testapp.vo.ObjectPKeyEntity;
+
+import java.util.List;
+
+@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
+ public interface IntPKeyDao {
+ @Insert
+ void insertMe(IntAutoIncPKeyEntity... items);
+ @Insert
+ long insertAndGetId(IntAutoIncPKeyEntity item);
+
+ @Insert
+ long[] insertAndGetIds(IntAutoIncPKeyEntity... item);
+
+ @Query("select * from IntAutoIncPKeyEntity WHERE pKey = :key")
+ IntAutoIncPKeyEntity getMe(int key);
+
+ @Query("select data from IntAutoIncPKeyEntity WHERE pKey IN(:ids)")
+ List<String> loadDataById(long... ids);
+ }
+
+ @Dao
+ public interface IntegerAutoIncPKeyDao {
+ @Insert
+ void insertMe(IntegerAutoIncPKeyEntity item);
+
+ @Query("select * from IntegerAutoIncPKeyEntity WHERE pKey = :key")
+ IntegerAutoIncPKeyEntity getMe(int key);
+
+ @Insert
+ long insertAndGetId(IntegerAutoIncPKeyEntity item);
+
+ @Insert
+ long[] insertAndGetIds(IntegerAutoIncPKeyEntity... item);
+
+ @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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
new file mode 100644
index 0000000..f993978
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/TestDatabase.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+import androidx.room.TypeConverter;
+import androidx.room.TypeConverters;
+import androidx.room.integration.testapp.dao.BlobEntityDao;
+import androidx.room.integration.testapp.dao.FunnyNamedDao;
+import androidx.room.integration.testapp.dao.PetCoupleDao;
+import androidx.room.integration.testapp.dao.PetDao;
+import androidx.room.integration.testapp.dao.ProductDao;
+import androidx.room.integration.testapp.dao.RawDao;
+import androidx.room.integration.testapp.dao.SchoolDao;
+import androidx.room.integration.testapp.dao.SpecificDogDao;
+import androidx.room.integration.testapp.dao.ToyDao;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.dao.UserPetDao;
+import androidx.room.integration.testapp.dao.WithClauseDao;
+import androidx.room.integration.testapp.vo.BlobEntity;
+import androidx.room.integration.testapp.vo.Day;
+import androidx.room.integration.testapp.vo.FunnyNamedEntity;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.PetCouple;
+import androidx.room.integration.testapp.vo.Product;
+import androidx.room.integration.testapp.vo.School;
+import androidx.room.integration.testapp.vo.Toy;
+import androidx.room.integration.testapp.vo.User;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
+ BlobEntity.class, Product.class, FunnyNamedEntity.class},
+ version = 1, exportSchema = false)
+@TypeConverters(TestDatabase.Converters.class)
+public abstract class TestDatabase extends RoomDatabase {
+ public abstract UserDao getUserDao();
+ public abstract PetDao getPetDao();
+ public abstract UserPetDao getUserPetDao();
+ public abstract SchoolDao getSchoolDao();
+ public abstract PetCoupleDao getPetCoupleDao();
+ public abstract ToyDao getToyDao();
+ public abstract BlobEntityDao getBlobEntityDao();
+ public abstract ProductDao getProductDao();
+ public abstract SpecificDogDao getSpecificDogDao();
+ public abstract WithClauseDao getWithClauseDao();
+ public abstract FunnyNamedDao getFunnyNamedDao();
+ public abstract RawDao getRawDao();
+
+ @SuppressWarnings("unused")
+ public static class Converters {
+ @TypeConverter
+ public Date fromTimestamp(Long value) {
+ return value == null ? null : new Date(value);
+ }
+
+ @TypeConverter
+ public Long dateToTimestamp(Date date) {
+ if (date == null) {
+ return null;
+ } else {
+ return date.getTime();
+ }
+ }
+
+ @TypeConverter
+ public Set<Day> decomposeDays(int flags) {
+ Set<Day> result = new HashSet<>();
+ for (Day day : Day.values()) {
+ if ((flags & (1 << day.ordinal())) != 0) {
+ result.add(day);
+ }
+ }
+ return result;
+ }
+
+ @TypeConverter
+ public int composeDays(Set<Day> days) {
+ int result = 0;
+ for (Day day : days) {
+ result |= 1 << day.ordinal();
+ }
+ return result;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/BlobEntityDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/BlobEntityDao.java
new file mode 100644
index 0000000..3c582fd
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/BlobEntityDao.java
@@ -0,0 +1,40 @@
+/*
+ * 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 androidx.room.integration.testapp.dao;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.integration.testapp.vo.BlobEntity;
+
+import java.util.List;
+
+@Dao
+public interface BlobEntityDao {
+
+ @Insert
+ void insert(BlobEntity item);
+
+ @Query("SELECT * FROM BlobEntity")
+ List<BlobEntity> selectAll();
+
+ @Query("SELECT content FROM BlobEntity WHERE id = :id")
+ byte[] getContent(long id);
+
+ @Query("UPDATE BlobEntity SET content = :content WHERE id = :id")
+ void updateContent(long id, byte[] content);
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/FunnyNamedDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/FunnyNamedDao.java
new file mode 100644
index 0000000..e6b57df
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/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 androidx.room.integration.testapp.dao;
+
+import static androidx.room.integration.testapp.vo.FunnyNamedEntity.COLUMN_ID;
+import static androidx.room.integration.testapp.vo.FunnyNamedEntity.TABLE_NAME;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Update;
+import androidx.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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/PetCoupleDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/PetCoupleDao.java
new file mode 100644
index 0000000..e027d77
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/PetCoupleDao.java
@@ -0,0 +1,33 @@
+/*
+ * 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 androidx.room.integration.testapp.dao;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.integration.testapp.vo.PetCouple;
+
+import java.util.List;
+
+@Dao
+public interface PetCoupleDao {
+ @Insert
+ void insert(PetCouple couple);
+
+ @Query("SELECT * FROM PetCouple")
+ List<PetCouple> loadAll();
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/PetDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/PetDao.java
new file mode 100644
index 0000000..ce63814
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/PetDao.java
@@ -0,0 +1,58 @@
+/*
+ * 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 androidx.room.integration.testapp.dao;
+
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Transaction;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.PetWithToyIds;
+
+import java.util.List;
+
+@Dao
+public interface PetDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertOrReplace(Pet... pets);
+
+ @Insert
+ void insertAll(Pet[] pets);
+
+ @Query("SELECT COUNT(*) FROM Pet")
+ int count();
+
+ @Query("SELECT * FROM Pet ORDER BY Pet.mPetId ASC")
+ List<PetWithToyIds> allPetsWithToyIds();
+
+ @Delete
+ void delete(Pet pet);
+
+ @Query("SELECT mPetId FROM Pet")
+ int[] allIds();
+
+ @Transaction
+ default void deleteAndInsert(Pet oldPet, Pet newPet, boolean shouldFail) {
+ delete(oldPet);
+ if (shouldFail) {
+ throw new RuntimeException();
+ }
+ insertOrReplace(newPet);
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/ProductDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/ProductDao.java
new file mode 100644
index 0000000..47b1a3b
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/ProductDao.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 androidx.room.integration.testapp.dao;
+
+import androidx.annotation.NonNull;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.integration.testapp.vo.Product;
+
+@Dao
+public interface ProductDao {
+
+ @Insert
+ long insert(@NonNull Product product);
+
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/RawDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/RawDao.java
new file mode 100644
index 0000000..e3638e4
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/RawDao.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.RawQuery;
+import androidx.room.integration.testapp.vo.NameAndLastName;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndPet;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import java.util.Date;
+import java.util.List;
+
+@Dao
+public interface RawDao {
+ @RawQuery
+ UserAndAllPets getUserAndAllPets(SupportSQLiteQuery query);
+
+ @RawQuery(observedEntities = UserAndAllPets.class)
+ LiveData<UserAndAllPets> getUserAndAllPetsObservable(SupportSQLiteQuery query);
+
+ @RawQuery
+ User getUser(SupportSQLiteQuery query);
+
+ @RawQuery
+ UserAndPet getUserAndPet(SupportSQLiteQuery query);
+
+ @RawQuery
+ NameAndLastName getUserNameAndLastName(SupportSQLiteQuery query);
+
+ @RawQuery(observedEntities = User.class)
+ NameAndLastName getUserNameAndLastNameWithObserved(SupportSQLiteQuery query);
+
+ @RawQuery
+ int count(SupportSQLiteQuery query);
+
+ @RawQuery
+ List<User> getUserList(SupportSQLiteQuery query);
+
+ @RawQuery
+ List<UserAndPet> getUserAndPetList(SupportSQLiteQuery query);
+
+ @RawQuery(observedEntities = UserAndPet.class)
+ LiveData<List<UserAndPet>> getUserAndPetListObservable(SupportSQLiteQuery query);
+
+ @RawQuery(observedEntities = User.class)
+ LiveData<User> getUserLiveData(SupportSQLiteQuery query);
+
+ @RawQuery
+ UserNameAndBirthday getUserAndBirthday(SupportSQLiteQuery query);
+
+ class UserNameAndBirthday {
+ @ColumnInfo(name = "mName")
+ public final String name;
+ @ColumnInfo(name = "mBirthday")
+ public final Date birthday;
+
+ public UserNameAndBirthday(String name, Date birthday) {
+ this.name = name;
+ this.birthday = birthday;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/SchoolDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/SchoolDao.java
new file mode 100644
index 0000000..634c451
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/SchoolDao.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 androidx.room.integration.testapp.dao;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.RoomWarnings;
+import androidx.room.integration.testapp.vo.Coordinates;
+import androidx.room.integration.testapp.vo.School;
+import androidx.room.integration.testapp.vo.SchoolRef;
+
+import java.util.List;
+
+@Dao
+public abstract class SchoolDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract void insert(School... schools);
+
+ @Query("SELECT * from School WHERE address_street LIKE '%' || :street || '%'")
+ public abstract List<School> findByStreet(String street);
+
+ @Query("SELECT mId, mName, manager_mName FROM School")
+ public abstract List<School> schoolAndManagerNames();
+
+ @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 mId, address_lat, address_lng FROM School WHERE mId = :schoolId")
+ public abstract School loadCoordinatesAsSchool(int schoolId);
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/SpecificDogDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/SpecificDogDao.java
new file mode 100644
index 0000000..f13e6df
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/SpecificDogDao.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 androidx.room.integration.testapp.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Query;
+import androidx.room.integration.testapp.vo.PetsToys;
+
+@Dao
+public interface SpecificDogDao {
+ @Query("SELECT 123 as petId")
+ LiveData<PetsToys> getSpecificDogsToys();
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/ToyDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/ToyDao.java
new file mode 100644
index 0000000..80f8801
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/ToyDao.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 androidx.room.integration.testapp.dao;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.integration.testapp.vo.Toy;
+
+@Dao
+public interface ToyDao {
+ @Insert
+ void insert(Toy... toys);
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
new file mode 100644
index 0000000..2fe26db
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserDao.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp.dao;
+
+import android.database.Cursor;
+
+import androidx.lifecycle.LiveData;
+import androidx.paging.DataSource;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.RawQuery;
+import androidx.room.Transaction;
+import androidx.room.Update;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.vo.AvgWeightByAge;
+import androidx.room.integration.testapp.vo.Day;
+import androidx.room.integration.testapp.vo.NameAndLastName;
+import androidx.room.integration.testapp.vo.User;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import org.reactivestreams.Publisher;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+
+@SuppressWarnings("SameParameterValue")
+@Dao
+public abstract class UserDao {
+
+ private final TestDatabase mDatabase;
+
+ public UserDao(TestDatabase database) {
+ mDatabase = database;
+ }
+
+ @Query("select * from user where mName like :name")
+ public abstract List<User> findUsersByName(String name);
+
+ @Query("select * from user where mId = :id")
+ public abstract User load(int id);
+
+ @Query("select * from user where mId IN(:ids)")
+ public abstract User[] loadByIds(int... ids);
+
+ @Query("select * from user where custommm = :customField")
+ public abstract List<User> findByCustomField(String customField);
+
+ @Insert
+ public abstract void insert(User user);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract void insertOrReplace(User user);
+
+ @Delete
+ public abstract int delete(User user);
+
+ @Delete
+ public abstract int deleteAll(User[] users);
+
+ @Query("delete from user")
+ public abstract int deleteEverything();
+
+ @Update
+ public abstract int update(User user);
+
+ @Update
+ public abstract int updateAll(List<User> users);
+
+ @Insert
+ public abstract void insertAll(User[] users);
+
+ @Query("select * from user where mAdmin = :isAdmin")
+ public abstract List<User> findByAdmin(boolean isAdmin);
+
+ @Query("delete from user where mAge > :age")
+ public abstract int deleteAgeGreaterThan(int age);
+
+ @Query("delete from user where mId IN(:uids)")
+ public abstract int deleteByUids(int... uids);
+
+ @Query("delete from user where mAge >= :min AND mAge <= :max")
+ public abstract int deleteByAgeRange(int min, int max);
+
+ @Query("update user set mName = :name where mId = :id")
+ public abstract int updateById(int id, String name);
+
+ @Query("update user set mId = mId + :amount")
+ public abstract void incrementIds(int amount);
+
+ @Query("update user set mAge = mAge + 1")
+ public abstract void incrementAgeOfAll();
+
+ @Query("select mId from user order by mId ASC")
+ public abstract List<Integer> loadIds();
+
+ @Query("select * from user where mId = :id")
+ public abstract LiveData<User> liveUserById(int id);
+
+ @Transaction
+ @Query("select * from user where mId = :id")
+ public abstract LiveData<User> liveUserByIdInTransaction(int id);
+
+ @Query("select * from user where mName LIKE '%' || :name || '%' ORDER BY mId DESC")
+ public abstract LiveData<List<User>> liveUsersListByName(String name);
+
+ @Query("select * from user where length(mName) = :length")
+ public abstract List<User> findByNameLength(int length);
+
+ @Query("select * from user where mAge = :age")
+ public abstract List<User> findByAge(int age);
+
+ @Query("select mAge, AVG(mWeight) from user GROUP BY mAge ORDER BY 2 DESC")
+ public abstract List<AvgWeightByAge> weightByAge();
+
+ @Query("select mAge, AVG(mWeight) from user GROUP BY mAge ORDER BY 2 DESC LIMIT 1")
+ public abstract LiveData<AvgWeightByAge> maxWeightByAgeGroup();
+
+ @Query("select * from user where mBirthday > :from AND mBirthday < :to")
+ public abstract List<User> findByBirthdayRange(Date from, Date to);
+
+ @Query("select mId from user where mId IN (:ids)")
+ public abstract Cursor findUsersAsCursor(int... ids);
+
+ @Query("select * from user where mId = :id")
+ public abstract Flowable<User> flowableUserById(int id);
+
+ @Query("select * from user where mId = :id")
+ public abstract Maybe<User> maybeUserById(int id);
+
+ @Query("select * from user where mId IN (:ids)")
+ public abstract Maybe<List<User>> maybeUsersByIds(int... ids);
+
+ @Query("select * from user where mId = :id")
+ public abstract Single<User> singleUserById(int id);
+
+ @Query("select * from user where mId IN (:ids)")
+ public abstract Single<List<User>> singleUsersByIds(int... ids);
+
+ @Query("select COUNT(*) from user")
+ public abstract Flowable<Integer> flowableCountUsers();
+
+ @Query("select COUNT(*) from user")
+ public abstract Publisher<Integer> publisherCountUsers();
+
+ @Query("SELECT mBirthday from User where mId = :id")
+ public abstract Date getBirthday(int id);
+
+ @Query("SELECT COUNT(*) from user")
+ public abstract int count();
+
+ @Query("SELECT mAdmin from User where mId = :uid")
+ public abstract boolean isAdmin(int uid);
+
+ @Query("SELECT mAdmin from User where mId = :uid")
+ public abstract LiveData<Boolean> isAdminLiveData(int uid);
+
+ public void insertBothByRunnable(final User a, final User b) {
+ mDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ insert(a);
+ insert(b);
+ }
+ });
+ }
+
+ public int insertBothByCallable(final User a, final User b) {
+ return mDatabase.runInTransaction(new Callable<Integer>() {
+ @Override
+ public Integer call() throws Exception {
+ insert(a);
+ insert(b);
+ return 2;
+ }
+ });
+ }
+
+ @Query("SELECT * FROM user where mAge > :age")
+ public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
+
+ @RawQuery(observedEntities = User.class)
+ public abstract DataSource.Factory<Integer, User> loadPagedByAgeWithObserver(
+ SupportSQLiteQuery query);
+
+ // TODO: switch to PositionalDataSource once Room supports it
+ @Query("SELECT * FROM user ORDER BY mAge DESC")
+ public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
+
+ @Query("DELETE FROM User WHERE mId IN (:ids) AND mAge == :age")
+ public abstract int deleteByAgeAndIds(int age, List<Integer> ids);
+
+ @Query("UPDATE User set mWeight = :weight WHERE mId IN (:ids) AND mAge == :age")
+ public abstract int updateByAgeAndIds(float weight, int age, List<Integer> ids);
+
+ @Query("SELECT * FROM user WHERE (mWorkDays & :days) != 0")
+ public abstract List<User> findUsersByWorkDays(Set<Day> days);
+
+ // QueryLoader
+
+ @Query("SELECT COUNT(*) from user")
+ public abstract Integer getUserCount();
+
+ @Query("SELECT u.mName, u.mLastName from user u where mId = :id")
+ public abstract NameAndLastName getNameAndLastName(int id);
+
+ @Transaction
+ public void insertBothByAnnotation(final User a, final User b) {
+ insert(a);
+ insert(b);
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.java
new file mode 100644
index 0000000..7fd47ef
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/UserPetDao.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 androidx.room.integration.testapp.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.paging.DataSource;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Update;
+import androidx.room.integration.testapp.vo.EmbeddedUserAndAllPets;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndPet;
+import androidx.room.integration.testapp.vo.UserAndPetAdoptionDates;
+import androidx.room.integration.testapp.vo.UserAndPetNonNull;
+import androidx.room.integration.testapp.vo.UserIdAndPetNames;
+import androidx.room.integration.testapp.vo.UserWithPetsAndToys;
+
+import java.util.List;
+
+import io.reactivex.Flowable;
+
+@Dao
+public interface UserPetDao {
+ @Query("SELECT * FROM User u, Pet p WHERE u.mId = p.mUserId")
+ List<UserAndPet> loadAll();
+
+ @Query("SELECT * FROM User u LEFT OUTER JOIN Pet p ON u.mId = p.mUserId")
+ List<UserAndPet> loadUsers();
+
+ @Query("SELECT * FROM User u LEFT OUTER JOIN Pet p ON u.mId = p.mUserId")
+ List<UserAndPetNonNull> loadUsersWithNonNullPet();
+
+ @Query("SELECT * FROM Pet p LEFT OUTER JOIN User u ON u.mId = p.mUserId")
+ List<UserAndPet> loadPets();
+
+ @Query("SELECT * FROM User u LEFT OUTER JOIN Pet p ON u.mId = p.mUserId")
+ DataSource.Factory<Integer, UserAndAllPets> dataSourceFactoryMultiTable();
+
+ @Query("SELECT * FROM User u")
+ List<UserAndAllPets> loadAllUsersWithTheirPets();
+
+ @Query("SELECT * FROM User u")
+ List<UserIdAndPetNames> loadUserAndPetNames();
+
+ @Query("SELECT * FROM User u")
+ List<UserWithPetsAndToys> loadUserWithPetsAndToys();
+
+ @Query("SELECT * FROM User UNION ALL SELECT * FROM USER")
+ List<UserAndAllPets> unionByItself();
+
+ @Query("SELECT * FROM User")
+ List<UserAndPetAdoptionDates> loadUserWithPetAdoptionDates();
+
+ @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);
+
+ @Insert
+ void insertUserAndPet(User user, Pet pet);
+
+ @Update
+ void updateUsersAndPets(User[] users, Pet[] pets);
+
+ @Delete
+ void delete2UsersAndPets(User user1, User user2, Pet[] pets);
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/WithClauseDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/WithClauseDao.java
new file mode 100644
index 0000000..615df10
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/WithClauseDao.java
@@ -0,0 +1,54 @@
+/*
+ * 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 androidx.room.integration.testapp.dao;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Query;
+
+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"
+ + " UNION ALL \n"
+ + " SELECT n+1, (n+1)*fact FROM factorial \n"
+ + " WHERE n < :maxIndexInclusive)\n"
+ + " SELECT fact FROM factorial;")
+ List<Integer> getFactorials(int maxIndexInclusive);
+
+ @Query("WITH RECURSIVE factorial(n, fact) AS \n"
+ + "(SELECT 0, 1 \n"
+ + " UNION ALL \n"
+ + " SELECT n+1, (n+1)*fact FROM factorial \n"
+ + " WHERE n < :maxIndexInclusive)\n"
+ + " SELECT mName FROM User WHERE User.mId IN (Select fact FROM factorial);")
+ List<String> getUsersWithFactorialIds(int maxIndexInclusive);
+
+ @Query("WITH RECURSIVE factorial(n, fact) AS \n"
+ + "(SELECT 0, 1 \n"
+ + " UNION ALL \n"
+ + " SELECT n+1, (n+1)*fact FROM factorial \n"
+ + " WHERE n < :maxIndexInclusive)\n"
+ + " SELECT mName FROM User WHERE User.mId IN (Select fact FROM factorial);")
+ LiveData<List<String>> getUsersWithFactorialIdsLiveData(int maxIndexInclusive);
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java
new file mode 100644
index 0000000..f1e2cfa
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationDb.java
@@ -0,0 +1,142 @@
+/*
+ * 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 androidx.room.integration.testapp.migration;
+
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.RoomDatabase;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.util.List;
+
+@SuppressWarnings("WeakerAccess")
+@Database(version = MigrationDb.LATEST_VERSION,
+ entities = {MigrationDb.Entity1.class, MigrationDb.Entity2.class,
+ MigrationDb.Entity4.class})
+public abstract class MigrationDb extends RoomDatabase {
+ static final int LATEST_VERSION = 7;
+ abstract MigrationDao dao();
+ @Entity(indices = {@Index(value = "name", unique = true)})
+ static class Entity1 {
+ public static final String TABLE_NAME = "Entity1";
+ @PrimaryKey
+ public int id;
+ public String name;
+ }
+
+ @Entity
+ static class Entity2 {
+ public static final String TABLE_NAME = "Entity2";
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String addedInV3;
+ public String name;
+ }
+
+ @Entity
+ static class Entity3 { // added in version 4, removed at 6
+ public static final String TABLE_NAME = "Entity3";
+ @PrimaryKey
+ public int id;
+ @Ignore //removed at 5
+ public String removedInV5;
+ public String name;
+ }
+
+ @Entity(foreignKeys = {
+ @ForeignKey(entity = Entity1.class,
+ parentColumns = "name",
+ childColumns = "name",
+ deferred = true)})
+ static class Entity4 {
+ public static final String TABLE_NAME = "Entity4";
+ @PrimaryKey
+ public int id;
+ @ColumnInfo(collate = ColumnInfo.NOCASE)
+ public String name;
+ }
+
+ @Dao
+ interface MigrationDao {
+ @Query("SELECT * from Entity1 ORDER BY id ASC")
+ List<Entity1> loadAllEntity1s();
+ @Query("SELECT * from Entity2 ORDER BY id ASC")
+ List<Entity2> loadAllEntity2s();
+ @Query("SELECT * from Entity2 ORDER BY id ASC")
+ List<Entity2Pojo> loadAllEntity2sAsPojo();
+ @Insert
+ void insert(Entity2... entity2);
+ }
+
+ static class Entity2Pojo extends Entity2 {
+ }
+
+ /**
+ * not a real dao because database will change.
+ */
+ static class Dao_V1 {
+ final SupportSQLiteDatabase mDb;
+
+ Dao_V1(SupportSQLiteDatabase db) {
+ mDb = db;
+ }
+
+ public void insertIntoEntity1(int id, String name) {
+ ContentValues values = new ContentValues();
+ values.put("id", id);
+ values.put("name", name);
+ long insertionId = mDb.insert(Entity1.TABLE_NAME,
+ SQLiteDatabase.CONFLICT_REPLACE, values);
+ if (insertionId == -1) {
+ throw new RuntimeException("test sanity failure");
+ }
+ }
+ }
+
+ /**
+ * not a real dao because database will change.
+ */
+ static class Dao_V2 {
+ final SupportSQLiteDatabase mDb;
+
+ Dao_V2(SupportSQLiteDatabase db) {
+ mDb = db;
+ }
+
+ public void insertIntoEntity2(int id, String name) {
+ ContentValues values = new ContentValues();
+ values.put("id", id);
+ values.put("name", name);
+ long insertionId = mDb.insert(Entity2.TABLE_NAME,
+ SQLiteDatabase.CONFLICT_REPLACE, values);
+ if (insertionId == -1) {
+ throw new RuntimeException("test sanity failure");
+ }
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
new file mode 100644
index 0000000..ce0e32a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
@@ -0,0 +1,434 @@
+/*
+ * 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 androidx.room.integration.testapp.migration;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Room;
+import androidx.room.migration.Migration;
+import androidx.room.testing.MigrationTestHelper;
+import androidx.room.util.TableInfo;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Test custom database migrations.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MigrationTest {
+ private static final String TEST_DB = "migration-test";
+ @Rule
+ public MigrationTestHelper helper;
+
+ public MigrationTest() {
+ helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
+ MigrationDb.class.getCanonicalName());
+ }
+
+ @Test
+ public void giveBadResource() throws IOException {
+ MigrationTestHelper helper = new MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ "foo", new FrameworkSQLiteOpenHelperFactory());
+ try {
+ helper.createDatabase(TEST_DB, 1);
+ throw new AssertionError("must have failed with missing file exception");
+ } catch (FileNotFoundException exception) {
+ assertThat(exception.getMessage(), containsString("Cannot find"));
+ }
+ }
+
+ @Test
+ public void startInCurrentVersion() throws IOException {
+ SupportSQLiteDatabase db = helper.createDatabase(TEST_DB,
+ MigrationDb.LATEST_VERSION);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
+ dao.insertIntoEntity1(2, "x");
+ db.close();
+ MigrationDb migrationDb = getLatestDb();
+ List<MigrationDb.Entity1> items = migrationDb.dao().loadAllEntity1s();
+ helper.closeWhenFinished(migrationDb);
+ assertThat(items.size(), is(1));
+ }
+
+ @Test
+ public void addTable() throws IOException {
+ SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(db);
+ dao.insertIntoEntity1(2, "foo");
+ dao.insertIntoEntity1(3, "bar");
+ db.close();
+ db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
+ new MigrationDb.Dao_V2(db).insertIntoEntity2(3, "blah");
+ db.close();
+ MigrationDb migrationDb = getLatestDb();
+ List<MigrationDb.Entity1> entity1s = migrationDb.dao().loadAllEntity1s();
+
+ assertThat(entity1s.size(), is(2));
+ MigrationDb.Entity2 entity2 = new MigrationDb.Entity2();
+ entity2.id = 2;
+ entity2.name = "bar";
+ // assert no error happens
+ migrationDb.dao().insert(entity2);
+ List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s();
+ assertThat(entity2s.size(), is(2));
+ }
+
+ private MigrationDb getLatestDb() {
+ MigrationDb db = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ MigrationDb.class, TEST_DB).addMigrations(ALL_MIGRATIONS).build();
+ // trigger open
+ db.beginTransaction();
+ db.endTransaction();
+ helper.closeWhenFinished(db);
+ return db;
+ }
+
+ @Test
+ public void addTableFailure() throws IOException {
+ testFailure(1, 2);
+ }
+
+ @Test
+ public void addColumnFailure() throws IOException {
+ SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
+ db.close();
+ IllegalStateException caught = null;
+ try {
+ helper.runMigrationsAndValidate(TEST_DB, 3, true, new EmptyMigration(2, 3));
+ } catch (IllegalStateException ex) {
+ caught = ex;
+ }
+ assertThat(caught, instanceOf(IllegalStateException.class));
+ }
+
+ @Test
+ public void addColumn() throws IOException {
+ SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 2);
+ MigrationDb.Dao_V2 v2Dao = new MigrationDb.Dao_V2(db);
+ v2Dao.insertIntoEntity2(7, "blah");
+ db.close();
+ helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_2_3);
+ // trigger open.
+ MigrationDb migrationDb = getLatestDb();
+ List<MigrationDb.Entity2> entity2s = migrationDb.dao().loadAllEntity2s();
+ assertThat(entity2s.size(), is(1));
+ assertThat(entity2s.get(0).name, is("blah"));
+ assertThat(entity2s.get(0).addedInV3, is(nullValue()));
+
+ List<MigrationDb.Entity2Pojo> entity2Pojos = migrationDb.dao().loadAllEntity2sAsPojo();
+ assertThat(entity2Pojos.size(), is(1));
+ assertThat(entity2Pojos.get(0).name, is("blah"));
+ assertThat(entity2Pojos.get(0).addedInV3, is(nullValue()));
+ }
+
+ @Test
+ public void failedToRemoveColumn() throws IOException {
+ testFailure(4, 5);
+ }
+
+ @Test
+ public void removeColumn() throws IOException {
+ helper.createDatabase(TEST_DB, 4);
+ final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+ 5, true, MIGRATION_4_5);
+ final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
+ assertThat(info.columns.size(), is(2));
+ }
+
+ @Test
+ public void dropTable() throws IOException {
+ helper.createDatabase(TEST_DB, 5);
+ final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+ 6, true, MIGRATION_5_6);
+ final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
+ assertThat(info.columns.size(), is(0));
+ }
+
+ @Test
+ public void failedToDropTable() throws IOException {
+ testFailure(5, 6);
+ }
+
+ @Test
+ public void failedToDropTableDontVerify() throws IOException {
+ helper.createDatabase(TEST_DB, 5);
+ final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+ 6, false, new EmptyMigration(5, 6));
+ final TableInfo info = TableInfo.read(db, MigrationDb.Entity3.TABLE_NAME);
+ assertThat(info.columns.size(), is(2));
+ }
+
+ @Test
+ public void failedForeignKey() throws IOException {
+ final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 6);
+ db.close();
+ Throwable throwable = null;
+ try {
+ helper.runMigrationsAndValidate(TEST_DB,
+ 7, false, new Migration(6, 7) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE Entity4 (`id` INTEGER NOT NULL,"
+ + " `name` TEXT, PRIMARY KEY(`id`))");
+ }
+ });
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertThat(throwable, instanceOf(IllegalStateException.class));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(), containsString("Migration failed"));
+ }
+
+ @Test
+ public void newTableWithForeignKey() throws IOException {
+ helper.createDatabase(TEST_DB, 6);
+ final SupportSQLiteDatabase db = helper.runMigrationsAndValidate(TEST_DB,
+ 7, false, MIGRATION_6_7);
+ final TableInfo info = TableInfo.read(db, MigrationDb.Entity4.TABLE_NAME);
+ assertThat(info.foreignKeys.size(), is(1));
+ }
+
+ @Test
+ public void missingMigration() throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
+ database.close();
+ try {
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .build();
+ db.dao().loadAllEntity1s();
+ throw new AssertionError("Should've failed :/");
+ } catch (IllegalStateException ignored) {
+ }
+ }
+
+ @Test
+ public void missingMigrationNuke() throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
+ dao.insertIntoEntity1(2, "foo");
+ dao.insertIntoEntity1(3, "bar");
+ database.close();
+
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .fallbackToDestructiveMigration()
+ .build();
+ assertThat(db.dao().loadAllEntity1s().size(), is(0));
+ db.close();
+ }
+
+ @Test
+ public void failWithIdentityCheck() throws IOException {
+ for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) {
+ String name = "test_" + i;
+ helper.createDatabase(name, i).close();
+ IllegalStateException exception = null;
+ try {
+ MigrationDb db = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ MigrationDb.class, name).build();
+ db.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ // do nothing
+ }
+ });
+ } catch (IllegalStateException ex) {
+ exception = ex;
+ }
+ MatcherAssert.assertThat("identity detection should've failed",
+ exception, notNullValue());
+ }
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_destructiveMigrationOccursForSuppliedVersion()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
+ dao.insertIntoEntity1(2, "foo");
+ dao.insertIntoEntity1(3, "bar");
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+
+ assertThat(db.dao().loadAllEntity1s().size(), is(0));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationStartVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_6_7)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationEndVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 5);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_5_6)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
+ private void testFailure(int startVersion, int endVersion) throws IOException {
+ final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
+ db.close();
+ Throwable throwable = null;
+ try {
+ helper.runMigrationsAndValidate(TEST_DB, endVersion, true,
+ new EmptyMigration(startVersion, endVersion));
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertThat(throwable, instanceOf(IllegalStateException.class));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(), containsString("Migration failed"));
+ }
+
+ private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` ("
+ + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
+ + " `name` TEXT)");
+ }
+ };
+
+ private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE " + MigrationDb.Entity2.TABLE_NAME
+ + " ADD COLUMN addedInV3 TEXT");
+ }
+ };
+
+ private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER NOT NULL,"
+ + " `removedInV5` TEXT, `name` TEXT, PRIMARY KEY(`id`))");
+ }
+ };
+
+ private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER NOT NULL,"
+ + " `name` TEXT, PRIMARY KEY(`id`))");
+ database.execSQL("INSERT INTO Entity3_New(`id`, `name`) "
+ + "SELECT `id`, `name` FROM Entity3");
+ database.execSQL("DROP TABLE Entity3");
+ database.execSQL("ALTER TABLE Entity3_New RENAME TO Entity3");
+ }
+ };
+
+ private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("DROP TABLE " + MigrationDb.Entity3.TABLE_NAME);
+ }
+ };
+
+ private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS " + MigrationDb.Entity4.TABLE_NAME
+ + " (`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`)");
+ }
+ };
+
+ private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_1_2,
+ MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7};
+
+ static final class EmptyMigration extends Migration {
+ EmptyMigration(int startVersion, int endVersion) {
+ super(startVersion, endVersion);
+ }
+
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ // do nothing
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/DataSourceFactoryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/DataSourceFactoryTest.java
new file mode 100644
index 0000000..92a43b7
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/DataSourceFactoryTest.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 androidx.room.integration.testapp.paging;
+
+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 static org.junit.Assert.assertNotNull;
+
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.paging.DataSource;
+import androidx.paging.LivePagedListBuilder;
+import androidx.paging.PagedList;
+import androidx.room.integration.testapp.test.TestDatabaseTest;
+import androidx.room.integration.testapp.test.TestUtil;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class DataSourceFactoryTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ private interface LivePagedListFactory {
+ LiveData<PagedList<User>> create();
+ }
+
+ @Test
+ public void getUsersAsPagedList()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
+ mUserDao.loadPagedByAge(3),
+ new PagedList.Config.Builder()
+ .setPageSize(10)
+ .setPrefetchDistance(1)
+ .setInitialLoadSizeHint(10).build())
+ .build());
+ }
+
+ @Test
+ public void getUsersAsPagedList_ViaRawQuery_WithObservable()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ SimpleSQLiteQuery query = new SimpleSQLiteQuery(
+ "SELECT * FROM user where mAge > ?",
+ new Object[]{3});
+ validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
+ mUserDao.loadPagedByAgeWithObserver(query),
+ new PagedList.Config.Builder()
+ .setPageSize(10)
+ .setPrefetchDistance(1)
+ .setInitialLoadSizeHint(10).build())
+ .build());
+ }
+
+ private void validateUsersAsPagedList(
+ LivePagedListFactory factory)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mDatabase.beginTransaction();
+ try {
+ for (int i = 0; i < 100; i++) {
+ final User user = TestUtil.createUser(i + 1);
+ user.setAge(i);
+ mUserDao.insert(user);
+ }
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+ assertThat(mUserDao.count(), is(100));
+
+ final LiveData<PagedList<User>> livePagedUsers = factory.create();
+
+ final TestLifecycleOwner testOwner = new TestLifecycleOwner();
+ testOwner.handleEvent(Lifecycle.Event.ON_CREATE);
+ drain();
+ PagedListObserver<User> observer = new PagedListObserver<>();
+
+ observe(livePagedUsers, testOwner, observer);
+ assertThat(observer.get(), nullValue());
+ observer.reset();
+
+ testOwner.handleEvent(Lifecycle.Event.ON_START);
+ drain();
+
+ PagedList<User> pagedList1 = observer.get();
+ assertThat(pagedList1, is(notNullValue()));
+
+ assertThat(pagedList1.size(), is(96));
+ assertThat(getAndLoad(pagedList1, 20), is(nullValue()));
+ drain();
+ assertThat(getAndLoad(pagedList1, 31), nullValue());
+ assertThat(getAndLoad(pagedList1, 20), notNullValue());
+ assertThat(getAndLoad(pagedList1, 16), notNullValue());
+
+ drain();
+ assertThat(getAndLoad(pagedList1, 31), notNullValue());
+ assertThat(getAndLoad(pagedList1, 50), nullValue());
+ drain();
+ assertThat(getAndLoad(pagedList1, 50), notNullValue());
+ observer.reset();
+ // now invalidate the database but don't get the new paged list
+ mUserDao.updateById(50, "foo");
+ assertThat(getAndLoad(pagedList1, 70), nullValue());
+ drain();
+ assertThat(getAndLoad(pagedList1, 70), nullValue());
+ PagedList<User> pagedList = observer.get();
+ assertThat(getAndLoad(pagedList, 70), notNullValue());
+ }
+
+ private <T> T getAndLoad(PagedList<T> list, int pos) {
+ T result = list.get(pos);
+ list.loadAround(pos);
+ return result;
+ }
+
+ private void drain() throws InterruptedException, TimeoutException {
+ mExecutorRule.drainTasks(60, TimeUnit.SECONDS);
+ }
+
+ private void observe(final LiveData liveData, final LifecycleOwner provider,
+ final Observer observer) throws ExecutionException, InterruptedException {
+ FutureTask<Void> futureTask = new FutureTask<>(() -> {
+ //noinspection unchecked
+ liveData.observe(provider, observer);
+ return null;
+ });
+ ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
+ futureTask.get();
+ }
+
+ @Test
+ public void withRelation() throws ExecutionException, InterruptedException, TimeoutException {
+ // verify DataSourceFactory can be created from a multi table join
+ DataSource.Factory<Integer, UserAndAllPets> factory =
+ mUserPetDao.dataSourceFactoryMultiTable();
+ LiveData<PagedList<UserAndAllPets>> liveData =
+ new LivePagedListBuilder<>(mUserPetDao.dataSourceFactoryMultiTable(), 10).build();
+ assertNotNull(factory.create());
+
+ PagedListObserver<UserAndAllPets> observer = new PagedListObserver<>();
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ observe(liveData, lifecycleOwner, observer);
+ drain();
+ assertThat(observer.get(), is(Collections.emptyList()));
+
+ observer.reset();
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ final UserAndAllPets noPets = observer.get().get(0);
+ assertThat(noPets.user, is(user));
+
+ observer.reset();
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mPetDao.insertAll(pets);
+
+ drain();
+ final UserAndAllPets withPets = observer.get().get(0);
+ assertThat(withPets.user, is(user));
+ assertThat(withPets.pets, is(Arrays.asList(pets)));
+ }
+
+ static class TestLifecycleOwner implements LifecycleOwner {
+
+ private LifecycleRegistry mLifecycle;
+
+ TestLifecycleOwner() {
+ mLifecycle = new LifecycleRegistry(this);
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycle;
+ }
+
+ void handleEvent(Lifecycle.Event event) {
+ mLifecycle.handleLifecycleEvent(event);
+ }
+ }
+
+ private static class PagedListObserver<T> implements Observer<PagedList<T>> {
+ private PagedList<T> mList;
+
+ void reset() {
+ mList = null;
+ }
+
+ public PagedList<T> get() {
+ return mList;
+ }
+
+ @Override
+ public void onChanged(@Nullable PagedList<T> pagedList) {
+ mList = pagedList;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
new file mode 100644
index 0000000..c4c6c16
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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 androidx.room.integration.testapp.paging;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.room.integration.testapp.test.TestDatabaseTest;
+import androidx.room.integration.testapp.test.TestUtil;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.paging.LimitOffsetDataSource;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LimitOffsetDataSourceTest extends TestDatabaseTest {
+
+ @After
+ public void teardown() {
+ mUserDao.deleteEverything();
+ }
+
+ private LimitOffsetDataSource<User> loadUsersByAgeDesc() {
+ return (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc().create();
+ }
+
+ @Test
+ public void emptyPage() {
+ LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
+ assertThat(dataSource.countItems(), is(0));
+ }
+
+ @Test
+ public void loadCount() {
+ createUsers(6);
+ LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
+ assertThat(dataSource.countItems(), is(6));
+ }
+
+ @Test
+ public void singleItem() {
+ List<User> users = createUsers(1);
+ LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
+ assertThat(dataSource.countItems(), is(1));
+ List<User> initial = dataSource.loadRange(0, 10);
+
+ assertThat(initial.get(0), is(users.get(0)));
+ assertFalse(dataSource.loadRange(1, 10).iterator().hasNext());
+ }
+
+ @Test
+ public void initial() {
+ List<User> users = createUsers(10);
+ LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
+ assertThat(dataSource.countItems(), is(10));
+ List<User> initial = dataSource.loadRange(0, 1);
+ assertThat(initial.get(0), is(users.get(0)));
+ List<User> second = dataSource.loadRange(1, 1);
+ assertThat(second.get(0), is(users.get(1)));
+ }
+
+ @Test
+ public void loadAll() {
+ List<User> users = createUsers(10);
+
+ LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
+ List<User> all = dataSource.loadRange(0, 10);
+ assertThat(users, is(all));
+ }
+
+ @Test
+ public void loadAfter() {
+ List<User> users = createUsers(10);
+ LimitOffsetDataSource<User> dataSource = loadUsersByAgeDesc();
+ List<User> result = dataSource.loadRange(4, 2);
+ assertThat(result, is(users.subList(4, 6)));
+ }
+
+ @NonNull
+ private List<User> createUsers(int count) {
+ List<User> users = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ User user = TestUtil.createUser(i);
+ user.setAge(1);
+ mUserDao.insert(user);
+ users.add(user);
+ }
+ return users;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ClearAllTablesTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ClearAllTablesTest.java
new file mode 100644
index 0000000..8bffdef
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ClearAllTablesTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
+import static org.junit.Assert.assertThat;
+
+import android.content.Context;
+import android.database.Cursor;
+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;
+
+import androidx.annotation.NonNull;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Insert;
+import androidx.room.InvalidationTracker;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SuppressWarnings("WeakerAccess")
+@RunWith(AndroidJUnit4.class)
+public class ClearAllTablesTest {
+
+ @Database(version = 1, entities = {Parent.class, Child.class}, exportSchema = false)
+ public abstract static class ClearAllTablesDatabase extends RoomDatabase {
+ abstract ClearAllTablesDao dao();
+ }
+
+ @Entity
+ public static class Parent {
+ @PrimaryKey
+ public long id;
+ public String name;
+
+ public Parent(long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ }
+
+ @Entity(foreignKeys = {
+ @ForeignKey(entity = Parent.class, parentColumns = "id", childColumns = "parentId")})
+ public static class Child {
+ @PrimaryKey
+ public long id;
+ public String name;
+ public long parentId;
+
+ public Child(long id, String name, long parentId) {
+ this.id = id;
+ this.name = name;
+ this.parentId = parentId;
+ }
+ }
+
+ @Dao
+ public interface ClearAllTablesDao {
+ @Insert
+ void insertParent(Parent parent);
+
+ @Insert
+ void insertChild(Child child);
+
+ @Query("SELECT COUNT(*) FROM Parent")
+ int countParent();
+
+ @Query("SELECT COUNT(*) FROM Child")
+ int countChild();
+ }
+
+ private ClearAllTablesDatabase mDatabase;
+ private ClearAllTablesDao mDao;
+
+ @Before
+ public void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ mDatabase = Room.inMemoryDatabaseBuilder(context, ClearAllTablesDatabase.class).build();
+ mDao = mDatabase.dao();
+ }
+
+ @After
+ public void closeDatabase() {
+ mDatabase.close();
+ }
+
+ @Test
+ @SmallTest
+ public void simple() {
+ mDao.insertParent(new Parent(1, "A"));
+ assertThat(mDao.countParent(), is(1));
+ mDatabase.clearAllTables();
+ assertThat(mDao.countParent(), is(0));
+ }
+
+ @Test
+ @SmallTest
+ public void foreignKey() {
+ mDao.insertParent(new Parent(1, "A"));
+ mDao.insertChild(new Child(1, "a", 1));
+ assertThat(mDao.countParent(), is(1));
+ assertThat(mDao.countChild(), is(1));
+ mDatabase.clearAllTables();
+ assertThat(mDao.countParent(), is(0));
+ assertThat(mDao.countChild(), is(0));
+ }
+
+ @Test
+ @SmallTest
+ public void observer() throws InterruptedException {
+ mDao.insertParent(new Parent(1, "A"));
+ assertThat(mDao.countParent(), is(1));
+ final CountDownLatch latch = new CountDownLatch(1);
+ mDatabase.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("Parent") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ assertThat(tables, hasSize(1));
+ assertThat(tables, hasItem("Parent"));
+ assertThat(mDao.countParent(), is(0));
+ latch.countDown();
+ }
+ });
+ mDatabase.clearAllTables();
+ assertThat(latch.await(3000, TimeUnit.MILLISECONDS), is(true));
+ }
+
+ @Test
+ @MediumTest
+ public void clearsDataFromDiskTruncate() throws IOException {
+ clearsDataFromDisk(RoomDatabase.JournalMode.TRUNCATE);
+ }
+
+ @Test
+ @MediumTest
+ @SdkSuppress(minSdkVersion = 16)
+ public void clearsDataFromDiskWal() throws IOException {
+ clearsDataFromDisk(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING);
+ }
+
+ private void clearsDataFromDisk(RoomDatabase.JournalMode journalMode) throws IOException {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final String dbName = "clear.db";
+ context.deleteDatabase(dbName);
+ ClearAllTablesDatabase db;
+ db = Room.databaseBuilder(context, ClearAllTablesDatabase.class, dbName)
+ .setJournalMode(journalMode).build();
+ final File file = context.getDatabasePath(dbName);
+ final String uuid = UUID.randomUUID().toString();
+ db.dao().insertParent(new Parent(1, uuid));
+ assertThat(queryEncoding(db), is(equalTo("UTF-8")));
+ db.close();
+ assertThat(containsString(file, uuid), is(true));
+ db = Room.databaseBuilder(context, ClearAllTablesDatabase.class, dbName)
+ .setJournalMode(journalMode).build();
+ db.clearAllTables();
+ db.close();
+ assertThat(containsString(file, uuid), is(false));
+ }
+
+ private String queryEncoding(RoomDatabase db) {
+ Cursor c = null;
+ try {
+ c = db.query("PRAGMA encoding", null);
+ c.moveToFirst();
+ return c.getString(0);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ private boolean containsString(File file, String s) throws IOException {
+ final byte[] content = new byte[(int) file.length()];
+ final FileInputStream stream = new FileInputStream(file);
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ assertThat(stream.read(content), is(content.length));
+ return new String(content, Charset.forName("UTF-8")).indexOf(s) > 0;
+ } finally {
+ stream.close();
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/CollationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/CollationTest.java
new file mode 100644
index 0000000..ca0d2e0
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/CollationTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CollationTest {
+ private CollateDb mDb;
+ private CollateDao mDao;
+ private Locale mDefaultLocale;
+ private final CollateEntity mItem1 = new CollateEntity(1, "abı");
+ private final CollateEntity mItem2 = new CollateEntity(2, "abi");
+ private final CollateEntity mItem3 = new CollateEntity(3, "abj");
+ private final CollateEntity mItem4 = new CollateEntity(4, "abç");
+
+ @Before
+ public void init() {
+ mDefaultLocale = Locale.getDefault();
+ }
+
+ private void initDao(Locale systemLocale) {
+ Locale.setDefault(systemLocale);
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ CollateDb.class).build();
+ mDao = mDb.dao();
+ mDao.insert(mItem1);
+ mDao.insert(mItem2);
+ mDao.insert(mItem3);
+ mDao.insert(mItem4);
+ }
+
+ @After
+ public void closeDb() {
+ mDb.close();
+ Locale.setDefault(mDefaultLocale);
+ }
+
+ @Test
+ public void localized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public void localized_asUnicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByLocalizedAsUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode_asLocalized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByUnicodeAsLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public void unicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @androidx.room.Entity
+ static class CollateEntity {
+ @PrimaryKey
+ public final int id;
+ @ColumnInfo(collate = ColumnInfo.LOCALIZED)
+ public final String localizedName;
+ @ColumnInfo(collate = ColumnInfo.UNICODE)
+ public final String unicodeName;
+
+ CollateEntity(int id, String name) {
+ this.id = id;
+ this.localizedName = name;
+ this.unicodeName = name;
+ }
+
+ CollateEntity(int id, String localizedName, String unicodeName) {
+ this.id = id;
+ this.localizedName = localizedName;
+ this.unicodeName = unicodeName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CollateEntity that = (CollateEntity) o;
+
+ if (id != that.id) return false;
+ if (!localizedName.equals(that.localizedName)) return false;
+ return unicodeName.equals(that.unicodeName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + localizedName.hashCode();
+ result = 31 * result + unicodeName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "CollateEntity{"
+ + "id=" + id
+ + ", localizedName='" + localizedName + '\''
+ + ", unicodeName='" + unicodeName + '\''
+ + '}';
+ }
+ }
+
+ @Dao
+ interface CollateDao {
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName ASC")
+ List<CollateEntity> sortedByLocalized();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName COLLATE UNICODE ASC")
+ List<CollateEntity> sortedByLocalizedAsUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName ASC")
+ List<CollateEntity> sortedByUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName COLLATE LOCALIZED ASC")
+ List<CollateEntity> sortedByUnicodeAsLocalized();
+
+ @Insert
+ void insert(CollateEntity... entities);
+ }
+
+ @Database(entities = CollateEntity.class, version = 1, exportSchema = false)
+ abstract static class CollateDb extends RoomDatabase {
+ abstract CollateDao dao();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ConstructorTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ConstructorTest.java
new file mode 100644
index 0000000..53475b3
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ConstructorTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Embedded;
+import androidx.room.Entity;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SuppressWarnings("SqlNoDataSourceInspection")
+@SmallTest
+public class ConstructorTest {
+ @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class,
+ EntityWithAnnotations.class},
+ exportSchema = false)
+ abstract static class MyDb extends RoomDatabase {
+ abstract MyDao dao();
+ }
+
+ @Dao
+ interface MyDao {
+ @Insert
+ void insertFull(FullConstructor... full);
+
+ @Query("SELECT * FROM fc WHERE a = :a")
+ FullConstructor loadFull(int a);
+
+ @Insert
+ void insertPartial(PartialConstructor... partial);
+
+ @Query("SELECT * FROM pc WHERE a = :a")
+ PartialConstructor loadPartial(int a);
+
+ @Insert
+ void insertEntityWithAnnotations(EntityWithAnnotations... items);
+
+ @Query("SELECT * FROM EntityWithAnnotations")
+ EntityWithAnnotations getEntitiWithAnnotations();
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(tableName = "fc")
+ static class FullConstructor {
+ @PrimaryKey
+ public final int a;
+ public final int b;
+ @Embedded
+ public final MyEmbedded embedded;
+
+ FullConstructor(int a, int b, MyEmbedded embedded) {
+ this.a = a;
+ this.b = b;
+ this.embedded = embedded;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FullConstructor that = (FullConstructor) o;
+
+ if (a != that.a) return false;
+ //noinspection SimplifiableIfStatement
+ if (b != that.b) return false;
+ return embedded != null ? embedded.equals(that.embedded)
+ : that.embedded == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = a;
+ result = 31 * result + b;
+ result = 31 * result + (embedded != null ? embedded.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(tableName = "pc")
+ static class PartialConstructor {
+ @PrimaryKey
+ public final int a;
+ public int b;
+ @Embedded
+ private MyEmbedded mEmbedded;
+
+ PartialConstructor(int a) {
+ this.a = a;
+ }
+
+ public MyEmbedded getEmbedded() {
+ return mEmbedded;
+ }
+
+ public void setEmbedded(MyEmbedded embedded) {
+ mEmbedded = embedded;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PartialConstructor that = (PartialConstructor) o;
+
+ if (a != that.a) return false;
+ //noinspection SimplifiableIfStatement
+ if (b != that.b) return false;
+ return mEmbedded != null ? mEmbedded.equals(that.mEmbedded)
+ : that.mEmbedded == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = a;
+ result = 31 * result + b;
+ result = 31 * result + (mEmbedded != null ? mEmbedded.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class MyEmbedded {
+ public final String text;
+
+ MyEmbedded(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MyEmbedded that = (MyEmbedded) o;
+
+ return text != null ? text.equals(that.text) : that.text == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return text != null ? text.hashCode() : 0;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity
+ static class EntityWithAnnotations {
+ @PrimaryKey
+ @NonNull
+ public final String id;
+
+ @NonNull
+ public final String username;
+
+ @Nullable
+ public final String displayName;
+
+ EntityWithAnnotations(
+ @NonNull String id,
+ @NonNull String username,
+ @Nullable String displayName) {
+ this.id = id;
+ this.username = username;
+ this.displayName = displayName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ EntityWithAnnotations that = (EntityWithAnnotations) o;
+
+ if (!id.equals(that.id)) return false;
+ if (!username.equals(that.username)) return false;
+ return displayName != null ? displayName.equals(that.displayName)
+ : that.displayName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id.hashCode();
+ result = 31 * result + username.hashCode();
+ result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
+ return result;
+ }
+ }
+
+ private MyDb mDb;
+ private MyDao mDao;
+
+ @Before
+ public void init() {
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), MyDb.class)
+ .build();
+ mDao = mDb.dao();
+ }
+
+ @Test
+ public void insertAndReadFullConstructor() {
+ FullConstructor inserted = new FullConstructor(1, 2, null);
+ mDao.insertFull(inserted);
+ final FullConstructor load = mDao.loadFull(1);
+ assertThat(load, is(inserted));
+ }
+
+ @Test
+ public void insertAndReadPartial() {
+ PartialConstructor item = new PartialConstructor(3);
+ item.b = 7;
+ mDao.insertPartial(item);
+ PartialConstructor load = mDao.loadPartial(3);
+ assertThat(load, is(item));
+ }
+
+ @Test // for bug b/69562125
+ public void entityWithAnnotations() {
+ EntityWithAnnotations item = new EntityWithAnnotations("a", "b", null);
+ mDao.insertEntityWithAnnotations(item);
+ assertThat(mDao.getEntitiWithAnnotations(), is(item));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/CustomDatabaseTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/CustomDatabaseTest.java
new file mode 100644
index 0000000..efe7bc1
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/CustomDatabaseTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.database.Customer;
+import androidx.room.integration.testapp.database.SampleDatabase;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class CustomDatabaseTest {
+
+ @Test
+ public void invalidationTrackerAfterClose() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ RoomDatabase.Builder<SampleDatabase> builder =
+ Room.databaseBuilder(context, SampleDatabase.class, "db")
+ .openHelperFactory(new RethrowExceptionFactory());
+ Customer customer = new Customer();
+ for (int i = 0; i < 100; i++) {
+ SampleDatabase db = builder.build();
+ db.getCustomerDao().insert(customer);
+ // Give InvalidationTracker enough time to start #mRefreshRunnable and pass the
+ // initialization check.
+ SystemClock.sleep(1);
+ // InvalidationTracker#mRefreshRunnable will cause race condition if its database query
+ // happens after close.
+ db.close();
+ }
+ }
+
+ /**
+ * This is mostly {@link FrameworkSQLiteOpenHelperFactory}, but the returned {@link
+ * SupportSQLiteDatabase} fails with {@link RuntimeException} instead of {@link
+ * IllegalStateException} or {@link SQLiteException}. This way, we can simulate custom database
+ * implementation that throws its own exception types.
+ */
+ private static class RethrowExceptionFactory implements SupportSQLiteOpenHelper.Factory {
+
+ @Override
+ public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
+ final FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory();
+ final SupportSQLiteOpenHelper helper = factory.create(configuration);
+ SupportSQLiteOpenHelper helperMock = mock(SupportSQLiteOpenHelper.class,
+ delegatesTo(helper));
+ // Inject mocks to the object hierarchy.
+ doAnswer(new Answer() {
+ @Override
+ public SupportSQLiteDatabase answer(InvocationOnMock invocation)
+ throws Throwable {
+ final SupportSQLiteDatabase db = helper.getWritableDatabase();
+ SupportSQLiteDatabase dbMock = mock(SupportSQLiteDatabase.class,
+ delegatesTo(db));
+ doAnswer(new Answer() {
+ @Override
+ public Cursor answer(InvocationOnMock invocation) throws Throwable {
+ SupportSQLiteQuery query = invocation.getArgument(0);
+ try {
+ return db.query(query);
+ } catch (IllegalStateException | SQLiteException e) {
+ // Rethrow the exception in order to simulate the way custom
+ // database implementation throws its own exception types.
+ throw new RuntimeException("closed", e);
+ }
+ }
+ }).when(dbMock).query(any(SupportSQLiteQuery.class));
+ return dbMock;
+ }
+ }).when(helperMock).getWritableDatabase();
+ return helperMock;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/DaoNameConflictTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/DaoNameConflictTest.java
new file mode 100644
index 0000000..d2a243e
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/DaoNameConflictTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DaoNameConflictTest {
+ private ConflictDatabase mDb;
+ @Before
+ public void init() {
+ mDb = Room.inMemoryDatabaseBuilder(
+ InstrumentationRegistry.getTargetContext(),
+ ConflictDatabase.class
+ ).build();
+ }
+
+ @After
+ public void close() {
+ mDb.close();
+ }
+
+ @Test
+ public void readFromItem1() {
+ Item1 item1 = new Item1(1, "a");
+ mDb.item1Dao().insert(item1);
+ Item2 item2 = new Item2(2, "b");
+ mDb.item2Dao().insert(item2);
+ assertThat(mDb.item1Dao().get(), is(item1));
+ assertThat(mDb.item2Dao().get(), is(item2));
+ }
+
+ @Entity
+ static class Item1 {
+ @PrimaryKey
+ public int id;
+ public String name;
+
+ Item1(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Dao
+ public interface Store {
+ @Query("SELECT * FROM Item1 LIMIT 1")
+ Item1 get();
+ @Insert
+ void insert(Item1... items);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Item1 item1 = (Item1) o;
+
+ //noinspection SimplifiableIfStatement
+ if (id != item1.id) return false;
+ return name != null ? name.equals(item1.name) : item1.name == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @Entity
+ static class Item2 {
+ @PrimaryKey
+ public int id;
+ public String name;
+
+ Item2(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Dao
+ public interface Store {
+ @Query("SELECT * FROM Item2 LIMIT 1")
+ Item2 get();
+ @Insert
+ void insert(Item2... items);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Item2 item2 = (Item2) o;
+
+ //noinspection SimplifiableIfStatement
+ if (id != item2.id) return false;
+ return name != null ? name.equals(item2.name) : item2.name == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @Database(version = 1, exportSchema = false, entities = {Item1.class, Item2.class})
+ public abstract static class ConflictDatabase extends RoomDatabase {
+ public abstract Item1.Store item1Dao();
+ public abstract Item2.Store item2Dao();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/DatabaseCallbackTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/DatabaseCallbackTest.java
new file mode 100644
index 0000000..5d48236
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/DatabaseCallbackTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.core.IsCollectionContaining.hasItem;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.vo.User;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class DatabaseCallbackTest {
+
+ @Test
+ @MediumTest
+ public void createAndOpen() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ TestDatabaseCallback callback1 = new TestDatabaseCallback();
+ TestDatabase db1 = null;
+ TestDatabase db2 = null;
+ try {
+ db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
+ .addCallback(callback1)
+ .build();
+ assertFalse(callback1.mCreated);
+ assertFalse(callback1.mOpened);
+ User user1 = TestUtil.createUser(3);
+ user1.setName("george");
+ db1.getUserDao().insert(user1);
+ assertTrue(callback1.mCreated);
+ assertTrue(callback1.mOpened);
+ TestDatabaseCallback callback2 = new TestDatabaseCallback();
+ db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
+ .addCallback(callback2)
+ .build();
+ assertFalse(callback2.mCreated);
+ assertFalse(callback2.mOpened);
+ User user2 = db2.getUserDao().load(3);
+ assertThat(user2.getName(), is("george"));
+ assertFalse(callback2.mCreated); // Not called; already created by db1
+ assertTrue(callback2.mOpened);
+ } finally {
+ if (db1 != null) {
+ db1.close();
+ }
+ if (db2 != null) {
+ db2.close();
+ }
+ assertTrue(context.deleteDatabase("test"));
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void writeOnCreate() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .addCallback(new RoomDatabase.Callback() {
+ @Override
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ Cursor cursor = null;
+ try {
+ cursor = db.query(
+ "SELECT name FROM sqlite_master WHERE type = 'table'");
+ ArrayList<String> names = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ names.add(cursor.getString(0));
+ }
+ assertThat(names, hasItem("User"));
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ })
+ .build();
+ List<Integer> ids = db.getUserDao().loadIds();
+ assertThat(ids, is(empty()));
+ }
+
+ public static class TestDatabaseCallback extends RoomDatabase.Callback {
+
+ boolean mCreated;
+ boolean mOpened;
+
+ @Override
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ mCreated = true;
+ }
+
+ @Override
+ public void onOpen(@NonNull SupportSQLiteDatabase db) {
+ mOpened = true;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EmbeddedTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EmbeddedTest.java
new file mode 100644
index 0000000..5d2400d
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/EmbeddedTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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 androidx.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.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.PetCoupleDao;
+import androidx.room.integration.testapp.dao.PetDao;
+import androidx.room.integration.testapp.dao.SchoolDao;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.dao.UserPetDao;
+import androidx.room.integration.testapp.vo.Coordinates;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.PetCouple;
+import androidx.room.integration.testapp.vo.School;
+import androidx.room.integration.testapp.vo.SchoolRef;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndPet;
+import androidx.room.integration.testapp.vo.UserAndPetNonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmbeddedTest {
+ private UserDao mUserDao;
+ private PetDao mPetDao;
+ private UserPetDao mUserPetDao;
+ private SchoolDao mSchoolDao;
+ private PetCoupleDao mPetCoupleDao;
+
+ @Before
+ public void createDb() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ mUserDao = db.getUserDao();
+ mPetDao = db.getPetDao();
+ mUserPetDao = db.getUserPetDao();
+ mSchoolDao = db.getSchoolDao();
+ mPetCoupleDao = db.getPetCoupleDao();
+ }
+
+ @Test
+ public void loadAll() {
+ Pet pet = TestUtil.createPet(1);
+ User user = TestUtil.createUser(2);
+ pet.setUserId(user.getId());
+ mUserDao.insert(user);
+ mPetDao.insertOrReplace(pet);
+ List<UserAndPet> all = mUserPetDao.loadAll();
+ assertThat(all.size(), is(1));
+ assertThat(all.get(0).getUser(), is(user));
+ assertThat(all.get(0).getPet(), is(pet));
+ }
+
+ @Test
+ public void loadFromUsers() {
+ Pet pet = TestUtil.createPet(1);
+ User user = TestUtil.createUser(2);
+ pet.setUserId(user.getId());
+ mUserDao.insert(user);
+ mPetDao.insertOrReplace(pet);
+ List<UserAndPet> all = mUserPetDao.loadUsers();
+ assertThat(all.size(), is(1));
+ assertThat(all.get(0).getUser(), is(user));
+ assertThat(all.get(0).getPet(), is(pet));
+ }
+
+ @Test
+ public void loadFromUsersWithNullPet() {
+ User user = TestUtil.createUser(2);
+ mUserDao.insert(user);
+ List<UserAndPet> all = mUserPetDao.loadUsers();
+ assertThat(all.size(), is(1));
+ assertThat(all.get(0).getUser(), is(user));
+ assertThat(all.get(0).getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void loadFromUsersWithNonNullPet() {
+ User user = TestUtil.createUser(2);
+ mUserDao.insert(user);
+ List<UserAndPetNonNull> all = mUserPetDao.loadUsersWithNonNullPet();
+ assertThat(all.size(), is(1));
+ assertThat(all.get(0).getUser(), is(user));
+ assertThat(all.get(0).getPet(), is(new Pet()));
+ }
+
+ @Test
+ public void loadFromPets() {
+ Pet pet = TestUtil.createPet(1);
+ User user = TestUtil.createUser(2);
+ pet.setUserId(user.getId());
+ mUserDao.insert(user);
+ mPetDao.insertOrReplace(pet);
+ List<UserAndPet> all = mUserPetDao.loadPets();
+ assertThat(all.size(), is(1));
+ assertThat(all.get(0).getUser(), is(user));
+ assertThat(all.get(0).getPet(), is(pet));
+ }
+
+ @Test
+ public void loadFromPetsWithNullUser() {
+ Pet pet = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet);
+ List<UserAndPet> all = mUserPetDao.loadPets();
+ assertThat(all.size(), is(1));
+ assertThat(all.get(0).getUser(), is(nullValue()));
+ assertThat(all.get(0).getPet(), is(pet));
+ }
+
+ @Test
+ public void findSchoolByStreet() {
+ School school = TestUtil.createSchool(3, 5);
+ school.getAddress().setStreet("foo");
+ mSchoolDao.insert(school);
+ List<School> result = mSchoolDao.findByStreet("foo");
+ assertThat(result.size(), is(1));
+ assertThat(result.get(0), is(school));
+ }
+
+ @Test
+ public void loadSubFieldsAsPojo() throws Exception {
+ loadSubFieldsTest(new Callable<List<School>>() {
+ @Override
+ public List<School> call() throws Exception {
+ List<School> result = new ArrayList<>();
+ for (SchoolRef ref : mSchoolDao.schoolAndManagerNamesAsPojo()) {
+ result.add(ref);
+ }
+ return result;
+ }
+ });
+ }
+
+ @Test
+ public void loadSubFieldsAsEntity() throws Exception {
+ loadSubFieldsTest(new Callable<List<School>>() {
+ @Override
+ public List<School> call() throws Exception {
+ return mSchoolDao.schoolAndManagerNames();
+ }
+ });
+ }
+
+ public void loadSubFieldsTest(Callable<List<School>> loader) throws Exception {
+ School school = TestUtil.createSchool(3, 5);
+ school.setName("MTV High");
+ school.getManager().setName("chet");
+ mSchoolDao.insert(school);
+
+ School school2 = TestUtil.createSchool(4, 6);
+ school2.setName("MTV Low");
+ school2.setManager(null);
+ mSchoolDao.insert(school2);
+
+ List<School> schools = loader.call();
+ assertThat(schools.size(), is(2));
+ assertThat(schools.get(0).getName(), is("MTV High"));
+ assertThat(schools.get(1).getName(), is("MTV Low"));
+ assertThat(schools.get(0).address, nullValue());
+ assertThat(schools.get(1).address, nullValue());
+ assertThat(schools.get(0).getManager(), notNullValue());
+ assertThat(schools.get(1).getManager(), nullValue());
+ assertThat(schools.get(0).getManager().getName(), is("chet"));
+ }
+
+ @Test
+ public void loadNestedSub() {
+ School school = TestUtil.createSchool(3, 5);
+ school.getAddress().getCoordinates().lat = 3.;
+ school.getAddress().getCoordinates().lng = 4.;
+ mSchoolDao.insert(school);
+ Coordinates coordinates = mSchoolDao.loadCoordinates(3);
+ assertThat(coordinates.lat, is(3.));
+ assertThat(coordinates.lng, is(4.));
+
+ School asSchool = mSchoolDao.loadCoordinatesAsSchool(3);
+ assertThat(asSchool.address.getCoordinates().lat, is(3.));
+ assertThat(asSchool.address.getCoordinates().lng, is(4.));
+ // didn't as for it so don't load
+ assertThat(asSchool.getManager(), nullValue());
+ assertThat(asSchool.address.getStreet(), nullValue());
+ }
+
+ @Test
+ public void sameFieldType() {
+ Pet male = TestUtil.createPet(3);
+ Pet female = TestUtil.createPet(5);
+ PetCouple petCouple = new PetCouple();
+ petCouple.id = "foo";
+ petCouple.male = male;
+ petCouple.setFemale(female);
+ mPetCoupleDao.insert(petCouple);
+ List<PetCouple> petCouples = mPetCoupleDao.loadAll();
+ assertThat(petCouples.size(), is(1));
+ PetCouple loaded = petCouples.get(0);
+ assertThat(loaded.id, is("foo"));
+ assertThat(loaded.male, is(male));
+ assertThat(loaded.getFemale(), is(female));
+ }
+
+ @Test
+ public void sameFieldOneNull() {
+ Pet loneWolf = TestUtil.createPet(3);
+ PetCouple petCouple = new PetCouple();
+ petCouple.id = "foo";
+ petCouple.male = loneWolf;
+ mPetCoupleDao.insert(petCouple);
+ List<PetCouple> petCouples = mPetCoupleDao.loadAll();
+ assertThat(petCouples.size(), is(1));
+ PetCouple loaded = petCouples.get(0);
+ assertThat(loaded.id, is("foo"));
+ assertThat(loaded.male, is(loneWolf));
+ assertThat(loaded.getFemale(), is(nullValue()));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ForeignKeyTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ForeignKeyTest.java
new file mode 100644
index 0000000..b843387
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/ForeignKeyTest.java
@@ -0,0 +1,426 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.both;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.either;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+
+import android.database.sqlite.SQLiteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Delete;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ForeignKeyTest {
+ @Database(version = 1, entities = {A.class, B.class, C.class, D.class, E.class},
+ exportSchema = false)
+ abstract static class ForeignKeyDb extends RoomDatabase {
+ abstract FkDao dao();
+ }
+
+ @SuppressWarnings({"SqlNoDataSourceInspection", "SameParameterValue"})
+ @Dao
+ interface FkDao {
+ @Insert
+ void insert(A... a);
+
+ @Insert
+ void insert(B... b);
+
+ @Insert
+ void insert(C... c);
+
+ @Insert
+ void insert(D... d);
+
+ @Query("SELECT * FROM A WHERE id = :id")
+ A loadA(int id);
+
+ @Query("SELECT * FROM B WHERE id = :id")
+ B loadB(int id);
+
+ @Query("SELECT * FROM C WHERE id = :id")
+ C loadC(int id);
+
+ @Query("SELECT * FROM D WHERE id = :id")
+ D loadD(int id);
+
+ @Query("SELECT * FROM E WHERE id = :id")
+ E loadE(int id);
+
+ @Delete
+ void delete(A... a);
+
+ @Delete
+ void delete(B... b);
+
+ @Delete
+ void delete(C... c);
+
+ @Query("UPDATE A SET name = :newName WHERE id = :id")
+ void changeNameA(int id, String newName);
+
+ @Insert
+ void insert(E... e);
+
+
+ }
+
+ @Entity(indices = {@Index(value = "name", unique = true),
+ @Index(value = {"name", "lastName"}, unique = true)})
+ static class A {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String name;
+ public String lastName;
+
+ A(String name) {
+ this.name = name;
+ }
+
+ @Ignore
+ A(String name, String lastName) {
+ this.name = name;
+ this.lastName = lastName;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(foreignKeys = {
+ @ForeignKey(entity = A.class,
+ parentColumns = "name",
+ childColumns = "aName")})
+
+ static class B {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String aName;
+
+ B(String aName) {
+ this.aName = aName;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(foreignKeys = {
+ @ForeignKey(entity = A.class,
+ parentColumns = "name",
+ childColumns = "aName",
+ deferred = true)})
+ static class C {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String aName;
+
+ C(String aName) {
+ this.aName = aName;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(foreignKeys = {
+ @ForeignKey(entity = A.class,
+ parentColumns = "name",
+ childColumns = "aName",
+ onDelete = ForeignKey.CASCADE,
+ onUpdate = ForeignKey.CASCADE)})
+ static class D {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String aName;
+
+ D(String aName) {
+ this.aName = aName;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity(foreignKeys = {
+ @ForeignKey(entity = A.class,
+ parentColumns = {"name", "lastName"},
+ childColumns = {"aName", "aLastName"},
+ onDelete = ForeignKey.SET_NULL,
+ onUpdate = ForeignKey.CASCADE)})
+ static class E {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String aName;
+ public String aLastName;
+
+ E() {
+ }
+
+ @Ignore
+ E(String aName, String aLastName) {
+ this.aName = aName;
+ this.aLastName = aLastName;
+ }
+ }
+
+
+ private ForeignKeyDb mDb;
+ private FkDao mDao;
+
+ @Before
+ public void openDb() {
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ ForeignKeyDb.class).build();
+ mDao = mDb.dao();
+ }
+
+ @Test
+ public void simpleForeignKeyFailure() {
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ mDao.insert(new B("foo"));
+ }
+ });
+ assertThat(t, instanceOf(SQLiteException.class));
+ assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
+ }
+
+ @Test
+ public void simpleForeignKeyDeferredFailure() {
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ mDao.insert(new C("foo"));
+ }
+ });
+ assertThat(t, instanceOf(SQLiteException.class));
+ assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
+ }
+
+ @Test
+ public void immediateForeignKeyFailure() {
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ try {
+ mDb.beginTransaction();
+ mDao.insert(new B("foo"));
+ mDao.insert(new A("foo"));
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+ });
+ assertThat(t, instanceOf(SQLiteException.class));
+ }
+
+ @Test
+ public void deferredForeignKeySuccess() {
+ try {
+ mDb.beginTransaction();
+ mDao.insert(new C("foo"));
+ mDao.insert(new A("foo"));
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ assertThat(mDao.loadA(1), notNullValue());
+ assertThat(mDao.loadC(1), notNullValue());
+ }
+
+ @Test
+ public void onDelete_noAction() {
+ mDao.insert(new A("a1"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new B("a1"));
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ mDao.delete(a);
+ }
+ });
+ assertThat(t, instanceOf(SQLiteException.class));
+ assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
+ }
+
+ @Test
+ public void onDelete_noAction_withTransaction() {
+ mDao.insert(new A("a1"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new B("a1"));
+ final B b = mDao.loadB(1);
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ deleteInTransaction(a, b);
+ }
+ });
+ assertThat(t, instanceOf(SQLiteException.class));
+ assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
+ }
+
+ @Test
+ public void onDelete_noAction_deferred() {
+ mDao.insert(new A("a1"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new C("a1"));
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ mDao.delete(a);
+ }
+ });
+ assertThat(t, instanceOf(SQLiteException.class));
+ assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
+ }
+
+ @Test
+ public void onDelete_noAction__deferredWithTransaction() {
+ mDao.insert(new A("a1"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new C("a1"));
+ final C c = mDao.loadC(1);
+ deleteInTransaction(a, c);
+ }
+
+ @Test
+ public void onDelete_cascade() {
+ mDao.insert(new A("a1"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new D("a1"));
+ final D d = mDao.loadD(1);
+ assertThat("test sanity", d, notNullValue());
+ mDao.delete(a);
+ assertThat(mDao.loadD(1), nullValue());
+ }
+
+ @Test
+ public void onUpdate_cascade() {
+ mDao.insert(new A("a1"));
+ mDao.insert(new D("a1"));
+ final D d = mDao.loadD(1);
+ assertThat("test sanity", d, notNullValue());
+ mDao.changeNameA(1, "bla");
+ assertThat(mDao.loadD(1).aName, equalTo("bla"));
+ assertThat(mDao.loadA(1).name, equalTo("bla"));
+ }
+
+ @Test
+ public void multipleReferences() {
+ mDao.insert(new A("a1", "a2"));
+ final A a = mDao.loadA(1);
+ assertThat("test sanity", a, notNullValue());
+ Throwable t = catchException(new ThrowingRunnable() {
+ @Override
+ public void run() throws Exception {
+ mDao.insert(new E("a1", "dsa"));
+ }
+ });
+ assertThat(t.getMessage().toUpperCase(Locale.US), is(foreignKeyErrorMessage()));
+ }
+
+ @Test
+ public void onDelete_setNull_multipleReferences() {
+ mDao.insert(new A("a1", "a2"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new E("a1", "a2"));
+ assertThat(mDao.loadE(1), notNullValue());
+ mDao.delete(a);
+ E e = mDao.loadE(1);
+ assertThat(e, notNullValue());
+ assertThat(e.aName, nullValue());
+ assertThat(e.aLastName, nullValue());
+ }
+
+ @Test
+ public void onUpdate_cascade_multipleReferences() {
+ mDao.insert(new A("a1", "a2"));
+ final A a = mDao.loadA(1);
+ mDao.insert(new E("a1", "a2"));
+ assertThat(mDao.loadE(1), notNullValue());
+ mDao.changeNameA(1, "foo");
+ assertThat(mDao.loadE(1), notNullValue());
+ assertThat(mDao.loadE(1).aName, equalTo("foo"));
+ assertThat(mDao.loadE(1).aLastName, equalTo("a2"));
+ }
+
+ private static Matcher<String> foreignKeyErrorMessage() {
+ return either(containsString("FOREIGN KEY"))
+ .or(both(containsString("CODE 19")).and(containsString("CONSTRAINT FAILED")));
+ }
+
+ @SuppressWarnings("Duplicates")
+ private void deleteInTransaction(A a, B b) {
+ mDb.beginTransaction();
+ try {
+ mDao.delete(a);
+ mDao.delete(b);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @SuppressWarnings("Duplicates")
+ private void deleteInTransaction(A a, C c) {
+ mDb.beginTransaction();
+ try {
+ mDao.delete(a);
+ mDao.delete(c);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ private static Throwable catchException(ThrowingRunnable throwingRunnable) {
+ try {
+ throwingRunnable.run();
+ } catch (Throwable t) {
+ return t;
+ }
+ throw new RuntimeException("didn't throw an exception");
+ }
+
+ private interface ThrowingRunnable {
+ void run() throws Exception;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/FunnyNamedDaoTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/FunnyNamedDaoTest.java
new file mode 100644
index 0000000..9d0da7f
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/FunnyNamedDaoTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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 androidx.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.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.room.integration.testapp.vo.FunnyNamedEntity;
+
+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];
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> mFunnyNamedDao.observableOne(2).observeForever(
+ 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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/GenericEntityTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/GenericEntityTest.java
new file mode 100644
index 0000000..6d7acae
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/GenericEntityTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GenericEntityTest {
+ private GenericDb mDb;
+ private GenericDao mDao;
+
+ @Before
+ public void init() {
+ mDb = Room.inMemoryDatabaseBuilder(
+ InstrumentationRegistry.getTargetContext(),
+ GenericDb.class
+ ).build();
+ mDao = mDb.getDao();
+ }
+
+ @After
+ public void close() {
+ mDb.close();
+ }
+
+ @Test
+ public void readWriteEntity() {
+ EntityItem item = new EntityItem("abc", "def");
+ mDao.insert(item);
+ EntityItem received = mDao.get("abc");
+ assertThat(received, is(item));
+ }
+
+ @Test
+ public void readPojo() {
+ EntityItem item = new EntityItem("abc", "def");
+ mDao.insert(item);
+ PojoItem received = mDao.getPojo("abc");
+ assertThat(received.id, is("abc"));
+ }
+
+ static class Item<P, F> {
+ @NonNull
+ @PrimaryKey
+ public final P id;
+ private F mField;
+
+ Item(@NonNull P id) {
+ this.id = id;
+ }
+
+ public F getField() {
+ return mField;
+ }
+
+ public void setField(F field) {
+ mField = field;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Item<?, ?> item = (Item<?, ?>) o;
+ //noinspection SimplifiableIfStatement
+ if (!id.equals(item.id)) return false;
+ return mField != null ? mField.equals(item.mField) : item.mField == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id.hashCode();
+ result = 31 * result + (mField != null ? mField.hashCode() : 0);
+ return result;
+ }
+ }
+
+ static class PojoItem extends Item<String, Integer> {
+ PojoItem(String id) {
+ super(id);
+ }
+ }
+
+ @Entity
+ static class EntityItem extends Item<String, Integer> {
+ public final String name;
+
+ EntityItem(String id, String name) {
+ super(id);
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ EntityItem that = (EntityItem) o;
+ return name != null ? name.equals(that.name) : that.name == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @Dao
+ public interface GenericDao {
+ @Insert
+ void insert(EntityItem... items);
+
+ @Query("SELECT * FROM EntityItem WHERE id = :id")
+ EntityItem get(String id);
+
+ @Query("SELECT * FROM EntityItem WHERE id = :id")
+ PojoItem getPojo(String id);
+ }
+
+ @Database(version = 1, entities = {EntityItem.class}, exportSchema = false)
+ public abstract static class GenericDb extends RoomDatabase {
+ abstract GenericDao getDao();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/IdentityDetectionTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/IdentityDetectionTest.java
new file mode 100644
index 0000000..feac6cd
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/IdentityDetectionTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.vo.User;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IdentityDetectionTest {
+ static final String TAG = "IdentityDetectionTest";
+ static final String DB_FILE_NAME = "identity_test_db";
+ TestDatabase mTestDatabase;
+ @Before
+ public void createTestDatabase() {
+ deleteDbFile();
+ }
+
+ @Test
+ public void reOpenWithoutIssues() {
+ openDb();
+ mTestDatabase.getUserDao().insert(TestUtil.createUser(3));
+ closeDb();
+ openDb();
+ User[] users = mTestDatabase.getUserDao().loadByIds(3);
+ assertThat(users.length, is(1));
+ }
+
+ @Test
+ public void reOpenChangedHash() {
+ openDb();
+ mTestDatabase.getUserDao().insert(TestUtil.createUser(3));
+ // change the hash
+ SupportSQLiteDatabase db = mTestDatabase.getOpenHelper().getWritableDatabase();
+ db.execSQL("UPDATE " + Room.MASTER_TABLE_NAME + " SET `identity_hash` = ?"
+ + " WHERE id = 42", new String[]{"bad hash"});
+ closeDb();
+ Throwable[] exceptions = new Throwable[1];
+ try {
+ openDb();
+ mTestDatabase.getUserDao().loadByIds(3);
+ } catch (Throwable t) {
+ exceptions[0] = t;
+ mTestDatabase = null;
+ }
+ assertThat(exceptions[0], instanceOf(IllegalStateException.class));
+ }
+
+ @Test
+ public void reOpenMasterTableDropped() {
+ openDb();
+ mTestDatabase.getUserDao().insert(TestUtil.createUser(3));
+ // drop the master table
+ SupportSQLiteDatabase db = mTestDatabase.getOpenHelper().getWritableDatabase();
+ db.execSQL("DROP TABLE " + Room.MASTER_TABLE_NAME);
+ closeDb();
+ try {
+ openDb();
+ mTestDatabase.getUserDao().loadByIds(3);
+ fail("Was expecting an exception.");
+ } catch (Throwable t) {
+ assertThat(t, instanceOf(IllegalStateException.class));
+ }
+ }
+
+ private void closeDb() {
+ mTestDatabase.close();
+ }
+
+ private void openDb() {
+ mTestDatabase = Room.databaseBuilder(InstrumentationRegistry.getTargetContext(),
+ TestDatabase.class, DB_FILE_NAME).build();
+ }
+
+ @After
+ public void clear() {
+ try {
+ if (mTestDatabase != null) {
+ closeDb();
+ }
+ deleteDbFile();
+ } catch (Throwable t) {
+ Log.e(TAG, "could not close test database", t);
+ throw t;
+ }
+ }
+
+ private void deleteDbFile() {
+ File testDb = InstrumentationRegistry.getTargetContext().getDatabasePath(DB_FILE_NAME);
+ testDb.delete();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/IndexingTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/IndexingTest.java
new file mode 100644
index 0000000..04fc428
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/IndexingTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IndexingTest {
+ @Entity(
+ tableName = "foo_table",
+ indices = {
+ @Index({"field1", "field2"}),
+ @Index(value = {"field2", "mId"}, unique = true),
+ @Index(value = {"field2"}, unique = true, name = "customIndex"),
+ })
+ static class Entity1 {
+ @PrimaryKey
+ public int mId;
+ public String field1;
+ public String field2;
+ @ColumnInfo(index = true, name = "my_field")
+ public String field3;
+ }
+
+ static class IndexInfo {
+ public String name;
+ @ColumnInfo(name = "tbl_name")
+ public String tableName;
+ public String sql;
+ }
+
+ @Dao
+ public interface SqlMasterDao {
+ @Query("SELECT * FROM sqlite_master WHERE type = 'index'")
+ List<IndexInfo> loadIndices();
+ }
+
+ @Database(entities = {Entity1.class}, version = 1, exportSchema = false)
+ abstract static class IndexingDb extends RoomDatabase {
+ abstract SqlMasterDao sqlMasterDao();
+ }
+
+ @Test
+ public void verifyIndices() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ IndexingDb db = Room.inMemoryDatabaseBuilder(context, IndexingDb.class).build();
+ List<IndexInfo> indices = db.sqlMasterDao().loadIndices();
+ assertThat(indices.size(), is(4));
+ for (IndexInfo info : indices) {
+ assertThat(info.tableName, is("foo_table"));
+ }
+ assertThat(indices.get(0).sql, is("CREATE INDEX `index_foo_table_field1_field2`"
+ + " ON `foo_table` (`field1`, `field2`)"));
+ assertThat(indices.get(1).sql, is("CREATE UNIQUE INDEX `index_foo_table_field2_mId`"
+ + " ON `foo_table` (`field2`, `mId`)"));
+ assertThat(indices.get(2).sql, is("CREATE UNIQUE INDEX `customIndex`"
+ + " ON `foo_table` (`field2`)"));
+ assertThat(indices.get(3).sql, is("CREATE INDEX `index_foo_table_my_field`"
+ + " ON `foo_table` (`my_field`)"));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTest.java
new file mode 100644
index 0000000..fe66855
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.room.InvalidationTracker;
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.vo.User;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests invalidation tracking.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InvalidationTest {
+ @Rule
+ public CountingTaskExecutorRule executorRule = new CountingTaskExecutorRule();
+ private UserDao mUserDao;
+ private TestDatabase mDb;
+
+ @Before
+ public void createDb() throws TimeoutException, InterruptedException {
+ Context context = InstrumentationRegistry.getTargetContext();
+ mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ mUserDao = mDb.getUserDao();
+ drain();
+ }
+
+ @After
+ public void closeDb() throws TimeoutException, InterruptedException {
+ mDb.close();
+ drain();
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ executorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+
+ @Test
+ public void testInvalidationOnUpdate() throws InterruptedException, TimeoutException {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ LoggingObserver observer = new LoggingObserver("User");
+ mDb.getInvalidationTracker().addObserver(observer);
+ drain();
+ mUserDao.updateById(3, "foo2");
+ drain();
+ assertThat(observer.getInvalidatedTables(), hasSize(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("User"));
+ }
+
+ @Test
+ public void testInvalidationOnDelete() throws InterruptedException, TimeoutException {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ LoggingObserver observer = new LoggingObserver("User");
+ mDb.getInvalidationTracker().addObserver(observer);
+ drain();
+ mUserDao.delete(user);
+ drain();
+ assertThat(observer.getInvalidatedTables(), hasSize(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("User"));
+ }
+
+ @Test
+ public void testInvalidationOnInsert() throws InterruptedException, TimeoutException {
+ LoggingObserver observer = new LoggingObserver("User");
+ mDb.getInvalidationTracker().addObserver(observer);
+ drain();
+ mUserDao.insert(TestUtil.createUser(3));
+ drain();
+ assertThat(observer.getInvalidatedTables(), hasSize(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("User"));
+ }
+
+ @Test
+ public void testDontInvalidateOnLateInsert() throws InterruptedException, TimeoutException {
+ LoggingObserver observer = new LoggingObserver("User");
+ mUserDao.insert(TestUtil.createUser(3));
+ drain();
+ mDb.getInvalidationTracker().addObserver(observer);
+ drain();
+ assertThat(observer.getInvalidatedTables(), nullValue());
+ }
+
+ @Test
+ public void testMultipleTables() throws InterruptedException, TimeoutException {
+ LoggingObserver observer = new LoggingObserver("User", "Pet");
+ mDb.getInvalidationTracker().addObserver(observer);
+ drain();
+ mUserDao.insert(TestUtil.createUser(3));
+ drain();
+ assertThat(observer.getInvalidatedTables(), hasSize(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("User"));
+ }
+
+ private static class LoggingObserver extends InvalidationTracker.Observer {
+ private Set<String> mInvalidatedTables;
+
+ LoggingObserver(String... tables) {
+ super(tables);
+ }
+
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ mInvalidatedTables = tables;
+ }
+
+ Set<String> getInvalidatedTables() {
+ return mInvalidatedTables;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
new file mode 100644
index 0000000..2b32a62
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LiveDataQueryTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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 androidx.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.os.Build;
+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;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.room.InvalidationTrackerTrojan;
+import androidx.room.integration.testapp.vo.AvgWeightByAge;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.PetsToys;
+import androidx.room.integration.testapp.vo.Toy;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Tests invalidation tracking.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LiveDataQueryTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void observeById() throws InterruptedException, ExecutionException, TimeoutException {
+ final LiveData<User> userLiveData = mUserDao.liveUserById(5);
+ final TestLifecycleOwner testOwner = new TestLifecycleOwner();
+ testOwner.handleEvent(Lifecycle.Event.ON_CREATE);
+ final TestObserver<User> observer = new TestObserver<>();
+ observe(userLiveData, testOwner, observer);
+ assertThat(observer.hasValue(), is(false));
+ observer.reset();
+
+ testOwner.handleEvent(Lifecycle.Event.ON_START);
+ assertThat(observer.get(), is(nullValue()));
+
+ // another id
+ observer.reset();
+ mUserDao.insert(TestUtil.createUser(7));
+ assertThat(observer.get(), is(nullValue()));
+
+ observer.reset();
+ final User u5 = TestUtil.createUser(5);
+ mUserDao.insert(u5);
+ assertThat(observer.get(), is(notNullValue()));
+
+ u5.setName("foo-foo-foo");
+ observer.reset();
+ mUserDao.insertOrReplace(u5);
+ final User updated = observer.get();
+ assertThat(updated, is(notNullValue()));
+ assertThat(updated.getName(), is("foo-foo-foo"));
+
+ testOwner.handleEvent(Lifecycle.Event.ON_STOP);
+ observer.reset();
+ u5.setName("baba");
+ mUserDao.insertOrReplace(u5);
+ assertThat(observer.hasValue(), is(false));
+ }
+
+ @Test
+ public void observeListQuery() throws InterruptedException, ExecutionException,
+ TimeoutException {
+ final LiveData<List<User>> userLiveData = mUserDao.liveUsersListByName("frida");
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<List<User>> observer = new TestObserver<>();
+ observe(userLiveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(Collections.<User>emptyList()));
+
+ observer.reset();
+ final User user1 = TestUtil.createUser(3);
+ user1.setName("dog frida");
+ mUserDao.insert(user1);
+ assertThat(observer.get(), is(Collections.singletonList(user1)));
+
+ observer.reset();
+ final User user2 = TestUtil.createUser(5);
+ user2.setName("does not match");
+ mUserDao.insert(user2);
+ assertThat(observer.get(), is(Collections.singletonList(user1)));
+
+ observer.reset();
+ user1.setName("i don't match either");
+ mUserDao.insertOrReplace(user1);
+ assertThat(observer.get(), is(Collections.<User>emptyList()));
+
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_STOP);
+
+ observer.reset();
+ final User user3 = TestUtil.createUser(9);
+ user3.setName("painter frida");
+ mUserDao.insertOrReplace(user3);
+ assertThat(observer.hasValue(), is(false));
+
+ observer.reset();
+ final User user4 = TestUtil.createUser(11);
+ user4.setName("friday");
+ mUserDao.insertOrReplace(user4);
+ assertThat(observer.hasValue(), is(false));
+
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ assertThat(observer.get(), is(Arrays.asList(user4, user3)));
+ }
+
+ @Test
+ public void liveDataWithPojo() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
+ users[0].setAge(10);
+ users[0].setWeight(15);
+
+ users[1].setAge(20);
+ users[1].setWeight(25);
+
+ users[2].setAge(20);
+ users[2].setWeight(26);
+
+ users[3].setAge(10);
+ users[3].setWeight(21);
+
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+
+ final TestObserver<AvgWeightByAge> observer = new TestObserver<>();
+ LiveData<AvgWeightByAge> liveData = mUserDao.maxWeightByAgeGroup();
+
+ observe(liveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(nullValue()));
+
+ observer.reset();
+ mUserDao.insertAll(users);
+ assertThat(observer.get(), is(new AvgWeightByAge(20, 25.5f)));
+
+ observer.reset();
+ User user3 = mUserDao.load(3);
+ user3.setWeight(79);
+ mUserDao.insertOrReplace(user3);
+
+ assertThat(observer.get(), is(new AvgWeightByAge(10, 50)));
+ }
+
+ @Test
+ public void withRelation() throws ExecutionException, InterruptedException, TimeoutException {
+ final LiveData<UserAndAllPets> liveData = mUserPetDao.liveUserWithPets(3);
+ final TestObserver<UserAndAllPets> observer = new TestObserver<>();
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ observe(liveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(nullValue()));
+
+ observer.reset();
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ final UserAndAllPets noPets = observer.get();
+ assertThat(noPets.user, is(user));
+
+ observer.reset();
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mPetDao.insertAll(pets);
+
+ final UserAndAllPets withPets = observer.get();
+ assertThat(withPets.user, is(user));
+ assertThat(withPets.pets, is(Arrays.asList(pets)));
+ }
+
+ @Test
+ public void withRelationOnly() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ LiveData<PetsToys> liveData = mSpecificDogDao.getSpecificDogsToys();
+
+ PetsToys expected = new PetsToys();
+ expected.petId = 123;
+
+ Toy toy = new Toy();
+ toy.setId(1);
+ toy.setPetId(123);
+ toy.setName("ball");
+
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<PetsToys> observer = new TestObserver<>();
+ observe(liveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(expected));
+
+ observer.reset();
+ expected.toys.add(toy);
+ mToyDao.insert(toy);
+ assertThat(observer.get(), is(expected));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ public void withWithClause() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ LiveData<List<String>> actual =
+ mWithClauseDao.getUsersWithFactorialIdsLiveData(0);
+ List<String> expected = new ArrayList<>();
+
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<List<String>> observer = new TestObserver<>();
+ observe(actual, lifecycleOwner, observer);
+ assertThat(observer.get(), is(expected));
+
+ observer.reset();
+ User user = new User();
+ user.setId(0);
+ user.setName("Zero");
+ mUserDao.insert(user);
+ assertThat(observer.get(), is(expected));
+
+ observer.reset();
+ user = new User();
+ user.setId(1);
+ user.setName("One");
+ mUserDao.insert(user);
+ expected.add("One");
+ assertThat(observer.get(), is(expected));
+
+ observer.reset();
+ user = new User();
+ user.setId(6);
+ user.setName("Six");
+ mUserDao.insert(user);
+ assertThat(observer.get(), is(expected));
+
+ actual = mWithClauseDao.getUsersWithFactorialIdsLiveData(3);
+ observe(actual, lifecycleOwner, observer);
+ expected.add("Six");
+ assertThat(observer.get(), is(expected));
+ }
+
+ @MediumTest
+ @Test
+ public void handleGc() throws ExecutionException, InterruptedException, TimeoutException {
+ LiveData<User> liveData = mUserDao.liveUserById(3);
+ final TestObserver<User> observer = new TestObserver<>();
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ observe(liveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(nullValue()));
+ observer.reset();
+ final User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ assertThat(observer.get(), is(notNullValue()));
+ observer.reset();
+ forceGc();
+ String name = UUID.randomUUID().toString();
+ mUserDao.updateById(3, name);
+ assertThat(observer.get().getName(), is(name));
+
+ // release references
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY);
+ }
+ });
+ WeakReference<LiveData> weakLiveData = new WeakReference<LiveData>(liveData);
+ //noinspection UnusedAssignment
+ liveData = null;
+ forceGc();
+ mUserDao.updateById(3, "Bar");
+ forceGc();
+ assertThat(InvalidationTrackerTrojan.countObservers(mDatabase.getInvalidationTracker()),
+ is(0));
+ assertThat(weakLiveData.get(), nullValue());
+ }
+
+ @Test
+ public void booleanLiveData() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ User user = TestUtil.createUser(3);
+ user.setAdmin(false);
+ LiveData<Boolean> adminLiveData = mUserDao.isAdminLiveData(3);
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<Boolean> observer = new TestObserver<>();
+ observe(adminLiveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(nullValue()));
+ mUserDao.insert(user);
+ assertThat(observer.get(), is(false));
+ user.setAdmin(true);
+ mUserDao.insertOrReplace(user);
+ assertThat(observer.get(), is(true));
+ }
+
+ private void observe(final LiveData liveData, final LifecycleOwner provider,
+ final Observer observer) throws ExecutionException, InterruptedException {
+ FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ //noinspection unchecked
+ liveData.observe(provider, observer);
+ return null;
+ }
+ });
+ ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
+ futureTask.get();
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+
+ private static void forceGc() {
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ }
+
+ static class TestLifecycleOwner implements LifecycleOwner {
+
+ private LifecycleRegistry mLifecycle;
+
+ TestLifecycleOwner() {
+ mLifecycle = new LifecycleRegistry(this);
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycle;
+ }
+
+ void handleEvent(Lifecycle.Event event) {
+ mLifecycle.handleLifecycleEvent(event);
+ }
+ }
+
+ private class TestObserver<T> implements Observer<T> {
+ private T mLastData;
+ private boolean mHasValue = false;
+
+ void reset() {
+ mHasValue = false;
+ mLastData = null;
+ }
+
+ @Override
+ public void onChanged(@Nullable T o) {
+ mLastData = o;
+ mHasValue = true;
+ }
+
+ boolean hasValue() throws TimeoutException, InterruptedException {
+ drain();
+ return mHasValue;
+ }
+
+ T get() throws InterruptedException, TimeoutException {
+ drain();
+ assertThat(hasValue(), is(true));
+ return mLastData;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MainThreadCheckTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MainThreadCheckTest.java
new file mode 100644
index 0000000..50ffe3f
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MainThreadCheckTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.arch.core.util.Function;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.TestDatabase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MainThreadCheckTest {
+
+ @Test
+ public void testMainThread() {
+ final Throwable error = test(false, new Function<TestDatabase, Void>() {
+ @Override
+ public Void apply(TestDatabase db) {
+ db.getUserDao().load(3);
+ return null;
+ }
+ });
+ assertThat(error, notNullValue());
+ assertThat(error, instanceOf(IllegalStateException.class));
+ }
+
+ @Test
+ public void testFlowableOnMainThread() {
+ final Throwable error = test(false, new Function<TestDatabase, Void>() {
+ @Override
+ public Void apply(TestDatabase db) {
+ db.getUserDao().flowableUserById(3);
+ return null;
+ }
+ });
+ assertThat(error, nullValue());
+ }
+
+ @Test
+ public void testLiveDataOnMainThread() {
+ final Throwable error = test(false, new Function<TestDatabase, Void>() {
+ @Override
+ public Void apply(TestDatabase db) {
+ db.getUserDao().liveUserById(3);
+ return null;
+ }
+ });
+ assertThat(error, nullValue());
+ }
+
+ @Test
+ public void testAllowMainThread() {
+ final Throwable error = test(true, new Function<TestDatabase, Void>() {
+ @Override
+ public Void apply(TestDatabase db) {
+ db.getUserDao().load(3);
+ return null;
+ }
+ });
+ assertThat(error, nullValue());
+ }
+
+ private Throwable test(boolean allowMainThread, final Function<TestDatabase, Void> fun) {
+ Context context = InstrumentationRegistry.getTargetContext();
+ final RoomDatabase.Builder<TestDatabase> builder = Room.inMemoryDatabaseBuilder(
+ context, TestDatabase.class);
+ if (allowMainThread) {
+ builder.allowMainThreadQueries();
+ }
+ final TestDatabase db = builder.build();
+ final AtomicReference<Throwable> error = new AtomicReference<>();
+ try {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ fun.apply(db);
+ } catch (Throwable t) {
+ error.set(t);
+ }
+ }
+ });
+ } finally {
+ db.close();
+ }
+ return error.get();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoTest.java
new file mode 100644
index 0000000..9678d14
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoTest.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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.vo.AvgWeightByAge;
+import androidx.room.integration.testapp.vo.User;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class PojoTest {
+ private UserDao mUserDao;
+
+ @Before
+ public void createDb() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ mUserDao = db.getUserDao();
+ }
+
+ @Test
+ public void weightsByAge() {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 10);
+ users[0].setAge(10);
+ users[0].setWeight(20);
+
+ users[1].setAge(10);
+ users[1].setWeight(30);
+
+ users[2].setAge(15);
+ users[2].setWeight(12);
+
+ users[3].setAge(35);
+ users[3].setWeight(55);
+
+ mUserDao.insertAll(users);
+ assertThat(mUserDao.weightByAge(), is(
+ Arrays.asList(
+ new AvgWeightByAge(35, 55),
+ new AvgWeightByAge(10, 25),
+ new AvgWeightByAge(15, 12)
+ )
+ ));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
new file mode 100644
index 0000000..b09584c
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PojoWithRelationTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.integration.testapp.vo.EmbeddedUserAndAllPets;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.PetWithToyIds;
+import androidx.room.integration.testapp.vo.Toy;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndPetAdoptionDates;
+import androidx.room.integration.testapp.vo.UserIdAndPetNames;
+import androidx.room.integration.testapp.vo.UserWithPetsAndToys;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PojoWithRelationTest extends TestDatabaseTest {
+ @Test
+ public void fetchAll() {
+ User[] users = TestUtil.createUsersArray(1, 2, 3);
+ Pet[][] userPets = new Pet[3][];
+ mUserDao.insertAll(users);
+ for (User user : users) {
+ Pet[] pets = TestUtil.createPetsForUser(user.getId(), user.getId() * 10,
+ user.getId() - 1);
+ mPetDao.insertAll(pets);
+ userPets[user.getId() - 1] = pets;
+ }
+ List<UserAndAllPets> usersAndPets = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(usersAndPets.size(), is(3));
+ assertThat(usersAndPets.get(0).user, is(users[0]));
+ assertThat(usersAndPets.get(0).pets, is(Collections.<Pet>emptyList()));
+
+ assertThat(usersAndPets.get(1).user, is(users[1]));
+ assertThat(usersAndPets.get(1).pets, is(Arrays.asList(userPets[1])));
+
+ assertThat(usersAndPets.get(2).user, is(users[2]));
+ assertThat(usersAndPets.get(2).pets, is(Arrays.asList(userPets[2])));
+ }
+
+ private void createData() {
+ User[] users = TestUtil.createUsersArray(1, 2);
+ mUserDao.insertAll(users);
+ Pet user1_pet1 = TestUtil.createPet(1);
+ user1_pet1.setUserId(1);
+ user1_pet1.setName("pet1");
+ mPetDao.insertOrReplace(user1_pet1);
+
+ Pet user1_pet2 = TestUtil.createPet(2);
+ user1_pet2.setUserId(1);
+ user1_pet2.setName("pet2");
+ mPetDao.insertOrReplace(user1_pet2);
+
+ Pet user2_pet1 = TestUtil.createPet(3);
+ user2_pet1.setUserId(2);
+ user2_pet1.setName("pet3");
+ mPetDao.insertOrReplace(user2_pet1);
+ }
+
+ @Test
+ public void fetchWithNames() {
+ createData();
+
+ List<UserIdAndPetNames> usersAndPets = mUserPetDao.loadUserAndPetNames();
+ assertThat(usersAndPets.size(), is(2));
+ assertThat(usersAndPets.get(0).userId, is(1));
+ assertThat(usersAndPets.get(1).userId, is(2));
+ assertThat(usersAndPets.get(0).names, is(Arrays.asList("pet1", "pet2")));
+ assertThat(usersAndPets.get(1).names, is(Collections.singletonList("pet3")));
+ }
+
+ @Test
+ public void nested() {
+ createData();
+ Toy pet1_toy1 = new Toy();
+ pet1_toy1.setName("toy1");
+ pet1_toy1.setPetId(1);
+ Toy pet1_toy2 = new Toy();
+ pet1_toy2.setName("toy2");
+ pet1_toy2.setPetId(1);
+ mToyDao.insert(pet1_toy1, pet1_toy2);
+ List<UserWithPetsAndToys> userWithPetsAndToys = mUserPetDao.loadUserWithPetsAndToys();
+ assertThat(userWithPetsAndToys.size(), is(2));
+ UserWithPetsAndToys first = userWithPetsAndToys.get(0);
+ List<Toy> toys = first.pets.get(0).toys;
+ assertThat(toys.get(0).getName(), is("toy1"));
+ assertThat(toys.get(1).getName(), is("toy2"));
+ assertThat(userWithPetsAndToys.get(1).pets.get(0).toys, is(Collections.<Toy>emptyList()));
+ }
+
+ @Test
+ public void duplicateParentField() {
+ User[] users = TestUtil.createUsersArray(1, 2);
+ Pet[] pets_1 = TestUtil.createPetsForUser(1, 1, 2);
+ Pet[] pets_2 = TestUtil.createPetsForUser(2, 10, 1);
+ mUserDao.insertAll(users);
+ mPetDao.insertAll(pets_1);
+ mPetDao.insertAll(pets_2);
+ List<UserAndAllPets> userAndAllPets = mUserPetDao.unionByItself();
+ assertThat(userAndAllPets.size(), is(4));
+ for (int i = 0; i < 4; i++) {
+ assertThat("user at " + i, userAndAllPets.get(i).user, is(users[i % 2]));
+ }
+ assertThat(userAndAllPets.get(0).pets, is(Arrays.asList(pets_1)));
+ assertThat(userAndAllPets.get(2).pets, is(Arrays.asList(pets_1)));
+
+ assertThat(userAndAllPets.get(1).pets, is(Arrays.asList(pets_2)));
+ assertThat(userAndAllPets.get(3).pets, is(Arrays.asList(pets_2)));
+ }
+
+ @Test
+ public void embeddedRelation() {
+ createData();
+ EmbeddedUserAndAllPets relationContainer = mUserPetDao.loadUserAndPetsAsEmbedded(1);
+ assertThat(relationContainer.getUserAndAllPets(), notNullValue());
+ assertThat(relationContainer.getUserAndAllPets().user.getId(), is(1));
+ assertThat(relationContainer.getUserAndAllPets().pets.size(), is(2));
+ }
+
+ @Test
+ public void boxedPrimitiveList() {
+ Pet pet1 = TestUtil.createPet(3);
+ Pet pet2 = TestUtil.createPet(5);
+
+ Toy pet1_toy1 = TestUtil.createToyForPet(pet1, 10);
+ Toy pet1_toy2 = TestUtil.createToyForPet(pet1, 20);
+ Toy pet2_toy1 = TestUtil.createToyForPet(pet2, 30);
+
+ mPetDao.insertOrReplace(pet1, pet2);
+ mToyDao.insert(pet1_toy1, pet1_toy2, pet2_toy1);
+
+ List<PetWithToyIds> petWithToyIds = mPetDao.allPetsWithToyIds();
+ //noinspection ArraysAsListWithZeroOrOneArgument
+ assertThat(petWithToyIds, is(
+ Arrays.asList(
+ new PetWithToyIds(pet1, Arrays.asList(10, 20)),
+ new PetWithToyIds(pet2, Arrays.asList(30)))
+ ));
+ }
+
+ @Test
+ public void viaTypeConverter() {
+ User user = TestUtil.createUser(3);
+ Pet pet1 = TestUtil.createPet(3);
+ Date date1 = new Date(300);
+ pet1.setAdoptionDate(date1);
+ Pet pet2 = TestUtil.createPet(5);
+ Date date2 = new Date(700);
+ pet2.setAdoptionDate(date2);
+
+ pet1.setUserId(3);
+ pet2.setUserId(3);
+ mUserDao.insert(user);
+ mPetDao.insertOrReplace(pet1, pet2);
+
+ List<UserAndPetAdoptionDates> adoptions =
+ mUserPetDao.loadUserWithPetAdoptionDates();
+
+ assertThat(adoptions, is(Arrays.asList(
+ new UserAndPetAdoptionDates(user, Arrays.asList(new Date(300), new Date(700)))
+ )));
+ }
+
+ @Test
+ public void largeRelation_child() {
+ User user = TestUtil.createUser(3);
+ List<Pet> pets = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ Pet pet = TestUtil.createPet(i + 1);
+ pet.setUserId(3);
+ }
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
+ List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(result.size(), is(1));
+ assertThat(result.get(0).user, is(user));
+ assertThat(result.get(0).pets, is(pets));
+ }
+
+ @Test
+ public void largeRelation_parent() {
+ final List<User> users = new ArrayList<>();
+ final List<Pet> pets = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ User user = TestUtil.createUser(i + 1);
+ users.add(user);
+ Pet pet = TestUtil.createPet(i + 1);
+ pet.setUserId(user.getId());
+ pets.add(pet);
+ }
+ mDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ mUserDao.insertAll(users.toArray(new User[users.size()]));
+ mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
+ }
+ });
+ List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(result.size(), is(2000));
+ for (int i = 0; i < 2000; i++) {
+ assertThat(result.get(i).user, is(users.get(i)));
+ assertThat(result.get(i).pets, is(Collections.singletonList(pets.get(i))));
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrimaryKeyTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrimaryKeyTest.java
new file mode 100644
index 0000000..8eea68b
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrimaryKeyTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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 androidx.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 static org.junit.Assert.assertNotNull;
+
+import android.database.sqlite.SQLiteConstraintException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.PKeyTestDatabase;
+import androidx.room.integration.testapp.vo.IntAutoIncPKeyEntity;
+import androidx.room.integration.testapp.vo.IntegerAutoIncPKeyEntity;
+import androidx.room.integration.testapp.vo.IntegerPKeyEntity;
+import androidx.room.integration.testapp.vo.ObjectPKeyEntity;
+
+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(),
+ PKeyTestDatabase.class).build();
+ }
+
+ @Test
+ public void integerTest() {
+ IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
+ entity.data = "foo";
+ mDatabase.integerAutoIncPKeyDao().insertMe(entity);
+ IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(1);
+ assertThat(loaded, notNullValue());
+ assertThat(loaded.data, is(entity.data));
+ }
+
+ @Test
+ public void dontOverrideNullable0() {
+ IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
+ entity.pKey = 0;
+ entity.data = "foo";
+ mDatabase.integerAutoIncPKeyDao().insertMe(entity);
+ IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(0);
+ assertThat(loaded, notNullValue());
+ assertThat(loaded.data, is(entity.data));
+ }
+
+ @Test
+ public void intTest() {
+ IntAutoIncPKeyEntity entity = new IntAutoIncPKeyEntity();
+ entity.data = "foo";
+ mDatabase.intPKeyDao().insertMe(entity);
+ IntAutoIncPKeyEntity loaded = mDatabase.intPKeyDao().getMe(1);
+ assertThat(loaded, notNullValue());
+ assertThat(loaded.data, is(entity.data));
+ }
+
+ @Test
+ public void getInsertedId() {
+ IntAutoIncPKeyEntity entity = new IntAutoIncPKeyEntity();
+ entity.data = "foo";
+ final long id = mDatabase.intPKeyDao().insertAndGetId(entity);
+ assertThat(mDatabase.intPKeyDao().getMe((int) id).data, is("foo"));
+ }
+
+ @Test
+ public void getInsertedIds() {
+ IntAutoIncPKeyEntity entity = new IntAutoIncPKeyEntity();
+ entity.data = "foo";
+ IntAutoIncPKeyEntity entity2 = new IntAutoIncPKeyEntity();
+ entity2.data = "foo2";
+ final long[] ids = mDatabase.intPKeyDao().insertAndGetIds(entity, entity2);
+ assertThat(mDatabase.intPKeyDao().loadDataById(ids), is(Arrays.asList("foo", "foo2")));
+ }
+
+ @Test
+ public void getInsertedIdFromInteger() {
+ IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
+ entity.data = "foo";
+ final long id = mDatabase.integerAutoIncPKeyDao().insertAndGetId(entity);
+ assertThat(mDatabase.integerAutoIncPKeyDao().getMe((int) id).data, is("foo"));
+ }
+
+ @Test
+ public void getInsertedIdsFromInteger() {
+ IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity();
+ entity.data = "foo";
+ IntegerAutoIncPKeyEntity entity2 = new IntegerAutoIncPKeyEntity();
+ entity2.data = "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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java
new file mode 100644
index 0000000..152141d
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/QueryTransactionTest.java
@@ -0,0 +1,460 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+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 androidx.annotation.NonNull;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LiveData;
+import androidx.paging.DataSource;
+import androidx.paging.LivePagedListBuilder;
+import androidx.paging.PagedList;
+import androidx.paging.PositionalDataSource;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Relation;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.RoomWarnings;
+import androidx.room.Transaction;
+import androidx.room.paging.LimitOffsetDataSource;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subscribers.TestSubscriber;
+
+@SmallTest
+@RunWith(Parameterized.class)
+@SuppressWarnings("CheckReturnValue")
+public class QueryTransactionTest {
+ @Rule
+ public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule();
+ private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0);
+ private TransactionDb mDb;
+ private final boolean mUseTransactionDao;
+ private Entity1Dao mDao;
+ private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest
+ .TestLifecycleOwner();
+
+ @NonNull
+ @Parameterized.Parameters(name = "useTransaction_{0}")
+ public static Boolean[] getParams() {
+ return new Boolean[]{false, true};
+ }
+
+ public QueryTransactionTest(boolean useTransactionDao) {
+ mUseTransactionDao = useTransactionDao;
+ }
+
+ @Before
+ public void initDb() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START));
+
+ resetTransactionCount();
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ TransactionDb.class).build();
+ mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao();
+ drain();
+ }
+
+ @After
+ public void closeDb() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY));
+ drain();
+ mDb.close();
+ }
+
+ @Test
+ public void readList() {
+ mDao.insert(new Entity1(1, "foo"));
+ resetTransactionCount();
+
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ List<Entity1> allEntities = mDao.allEntities();
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void liveData() {
+ LiveData<List<Entity1>> listLiveData = mDao.liveData();
+ observeForever(listLiveData);
+ drain();
+ assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList()));
+
+ resetTransactionCount();
+ mDao.insert(new Entity1(1, "foo"));
+ drain();
+
+ //noinspection ConstantConditions
+ assertThat(listLiveData.getValue().size(), is(1));
+ int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+ assertTransactionCount(listLiveData.getValue(), expectedTransactionCount);
+ }
+
+ @Test
+ public void flowable() {
+ Flowable<List<Entity1>> flowable = mDao.flowable();
+ TestSubscriber<List<Entity1>> subscriber = observe(flowable);
+ drain();
+ assertThat(subscriber.values().size(), is(1));
+
+ resetTransactionCount();
+ mDao.insert(new Entity1(1, "foo"));
+ drain();
+
+ List<Entity1> allEntities = subscriber.values().get(1);
+ assertThat(allEntities.size(), is(1));
+ int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void maybe() {
+ mDao.insert(new Entity1(1, "foo"));
+ resetTransactionCount();
+
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ Maybe<List<Entity1>> listMaybe = mDao.maybe();
+ TestObserver<List<Entity1>> observer = observe(listMaybe);
+ drain();
+ List<Entity1> allEntities = observer.values().get(0);
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void single() {
+ mDao.insert(new Entity1(1, "foo"));
+ resetTransactionCount();
+
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ Single<List<Entity1>> listMaybe = mDao.single();
+ TestObserver<List<Entity1>> observer = observe(listMaybe);
+ drain();
+ List<Entity1> allEntities = observer.values().get(0);
+ assertTransactionCount(allEntities, expectedTransactionCount);
+ }
+
+ @Test
+ public void relation() {
+ mDao.insert(new Entity1(1, "foo"));
+ mDao.insert(new Child(1, 1));
+ mDao.insert(new Child(2, 1));
+ resetTransactionCount();
+
+ List<Entity1WithChildren> result = mDao.withRelation();
+ int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+ assertTransactionCountWithChildren(result, expectedTransactionCount);
+ }
+
+ @Test
+ public void pagedList() {
+ LiveData<PagedList<Entity1>> pagedList =
+ new LivePagedListBuilder<>(mDao.pagedList(), 10).build();
+ observeForever(pagedList);
+ drain();
+ assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0));
+
+ mDao.insert(new Entity1(1, "foo"));
+ drain();
+ //noinspection ConstantConditions
+ assertThat(pagedList.getValue().size(), is(1));
+ assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 2 : 1);
+
+ mDao.insert(new Entity1(2, "bar"));
+ drain();
+ assertThat(pagedList.getValue().size(), is(2));
+ assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 4 : 2);
+ }
+
+ @Test
+ public void dataSource() {
+ mDao.insert(new Entity1(2, "bar"));
+ drain();
+ resetTransactionCount();
+ @SuppressWarnings("deprecation")
+ LimitOffsetDataSource<Entity1> dataSource =
+ (LimitOffsetDataSource<Entity1>) mDao.dataSource();
+ dataSource.loadRange(0, 10);
+ assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+ }
+
+ private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) {
+ assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+ assertThat(allEntities.isEmpty(), is(false));
+ for (Entity1 entity1 : allEntities) {
+ assertThat(entity1.transactionId, is(expectedTransactionCount));
+ }
+ }
+
+ private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities,
+ int expectedTransactionCount) {
+ assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+ assertThat(allEntities.isEmpty(), is(false));
+ for (Entity1WithChildren entity1 : allEntities) {
+ assertThat(entity1.transactionId, is(expectedTransactionCount));
+ assertThat(entity1.children, notNullValue());
+ assertThat(entity1.children.isEmpty(), is(false));
+ for (Child child : entity1.children) {
+ assertThat(child.transactionId, is(expectedTransactionCount));
+ }
+ }
+ }
+
+ private void resetTransactionCount() {
+ sStartedTransactionCount.set(0);
+ }
+
+ private void drain() {
+ try {
+ countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new AssertionError("interrupted", e);
+ } catch (TimeoutException e) {
+ throw new AssertionError("drain timed out", e);
+ }
+ }
+
+ private <T> TestSubscriber<T> observe(final Flowable<T> flowable) {
+ TestSubscriber<T> subscriber = new TestSubscriber<>();
+ flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(subscriber);
+ return subscriber;
+ }
+
+ private <T> TestObserver<T> observe(final Maybe<T> maybe) {
+ TestObserver<T> observer = new TestObserver<>();
+ maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(observer);
+ return observer;
+ }
+
+ private <T> TestObserver<T> observe(final Single<T> single) {
+ TestObserver<T> observer = new TestObserver<>();
+ single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+ .subscribeWith(observer);
+ return observer;
+ }
+
+ private <T> void observeForever(final LiveData<T> liveData) {
+ FutureTask<Void> futureTask = new FutureTask<>(() -> {
+ liveData.observe(mLifecycleOwner, t -> {
+ });
+ return null;
+ });
+ ArchTaskExecutor.getMainThreadExecutor().execute(futureTask);
+ try {
+ futureTask.get();
+ } catch (InterruptedException e) {
+ throw new AssertionError("interrupted", e);
+ } catch (ExecutionException e) {
+ throw new AssertionError("execution error", e);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ static class Entity1WithChildren extends Entity1 {
+ @Relation(entity = Child.class, parentColumn = "id",
+ entityColumn = "entity1Id")
+ public List<Child> children;
+
+ Entity1WithChildren(int id, String value) {
+ super(id, value);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity
+ static class Child {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public int entity1Id;
+ @Ignore
+ public final int transactionId = sStartedTransactionCount.get();
+
+ Child(int id, int entity1Id) {
+ this.id = id;
+ this.entity1Id = entity1Id;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Entity
+ static class Entity1 {
+ @PrimaryKey(autoGenerate = true)
+ public int id;
+ public String value;
+ @Ignore
+ public final int transactionId = sStartedTransactionCount.get();
+
+ Entity1(int id, String value) {
+ this.id = id;
+ this.value = value;
+ }
+ }
+
+ // we don't support dao inheritance for queries so for now, go with this
+ interface Entity1Dao {
+ String SELECT_ALL = "select * from Entity1";
+
+ List<Entity1> allEntities();
+
+ Flowable<List<Entity1>> flowable();
+
+ Maybe<List<Entity1>> maybe();
+
+ Single<List<Entity1>> single();
+
+ LiveData<List<Entity1>> liveData();
+
+ List<Entity1WithChildren> withRelation();
+
+ DataSource.Factory<Integer, Entity1> pagedList();
+
+ PositionalDataSource<Entity1> dataSource();
+
+ @Insert
+ void insert(Entity1 entity1);
+
+ @Insert
+ void insert(Child entity1);
+ }
+
+ @Dao
+ interface EntityDao extends Entity1Dao {
+ @Override
+ @Query(SELECT_ALL)
+ List<Entity1> allEntities();
+
+ @Override
+ @Query(SELECT_ALL)
+ Flowable<List<Entity1>> flowable();
+
+ @Override
+ @Query(SELECT_ALL)
+ LiveData<List<Entity1>> liveData();
+
+ @Override
+ @Query(SELECT_ALL)
+ Maybe<List<Entity1>> maybe();
+
+ @Override
+ @Query(SELECT_ALL)
+ Single<List<Entity1>> single();
+
+ @Override
+ @Query(SELECT_ALL)
+ @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+ List<Entity1WithChildren> withRelation();
+
+ @Override
+ @Query(SELECT_ALL)
+ DataSource.Factory<Integer, Entity1> pagedList();
+
+ @Override
+ @Query(SELECT_ALL)
+ PositionalDataSource<Entity1> dataSource();
+ }
+
+ @Dao
+ interface TransactionDao extends Entity1Dao {
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ List<Entity1> allEntities();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ Flowable<List<Entity1>> flowable();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ LiveData<List<Entity1>> liveData();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ Maybe<List<Entity1>> maybe();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ Single<List<Entity1>> single();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ List<Entity1WithChildren> withRelation();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ DataSource.Factory<Integer, Entity1> pagedList();
+
+ @Override
+ @Transaction
+ @Query(SELECT_ALL)
+ PositionalDataSource<Entity1> dataSource();
+ }
+
+ @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
+ abstract static class TransactionDb extends RoomDatabase {
+ abstract EntityDao dao();
+
+ abstract TransactionDao transactionDao();
+
+ @Override
+ public void beginTransaction() {
+ super.beginTransaction();
+ sStartedTransactionCount.incrementAndGet();
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RawQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RawQueryTest.java
new file mode 100644
index 0000000..e577c5b
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RawQueryTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Collections.emptyList;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.lifecycle.LiveData;
+import androidx.room.integration.testapp.dao.RawDao;
+import androidx.room.integration.testapp.vo.NameAndLastName;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+import androidx.room.integration.testapp.vo.UserAndPet;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RawQueryTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void entity_null() {
+ User user = mRawDao.getUser(new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = 0"));
+ assertThat(user, is(nullValue()));
+ }
+
+ @Test
+ public void entity_one() {
+ User expected = TestUtil.createUser(3);
+ mUserDao.insert(expected);
+ User received = mRawDao.getUser(new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3}));
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_list() {
+ List<User> expected = TestUtil.createUsersList(1, 2, 3, 4);
+ mUserDao.insertAll(expected.toArray(new User[4]));
+ List<User> received = mRawDao.getUserList(
+ new SimpleSQLiteQuery("SELECT * FROM User ORDER BY mId ASC"));
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_liveData_string() throws TimeoutException, InterruptedException {
+ SupportSQLiteQuery query = new SimpleSQLiteQuery(
+ "SELECT * FROM User WHERE mId = ?",
+ new Object[]{3}
+ );
+ liveDataTest(mRawDao.getUserLiveData(query));
+ }
+
+ @Test
+ public void entity_liveData_supportQuery() throws TimeoutException, InterruptedException {
+ liveDataTest(mRawDao.getUserLiveData(
+ new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?", new Object[]{3})));
+ }
+
+ private void liveDataTest(
+ final LiveData<User> liveData) throws TimeoutException, InterruptedException {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> liveData.observeForever(user -> { }));
+ drain();
+ assertThat(liveData.getValue(), is(nullValue()));
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ user.setLastName("cxZ");
+ mUserDao.insertOrReplace(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ }
+
+ @Test
+ public void entity_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3});
+ User received = mRawDao.getUser(query);
+ assertThat(received, is(user));
+ }
+
+ @Test
+ public void embedded() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet(new SimpleSQLiteQuery(
+ "SELECT * FROM User, Pet WHERE User.mId = Pet.mUserId LIMIT 1"));
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(pets[0]));
+ }
+
+ @Test
+ public void relation() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 10);
+ mPetDao.insertAll(pets);
+ UserAndAllPets result = mRawDao
+ .getUserAndAllPets(new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3}));
+ assertThat(result.user, is(user));
+ assertThat(result.pets, is(Arrays.asList(pets)));
+ }
+
+ @Test
+ public void pojo() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName(new SimpleSQLiteQuery("SELECT * FROM User"));
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastNameWithObserved(new SimpleSQLiteQuery(
+ "SELECT * FROM User WHERE mId = ?",
+ new Object[]{3}
+ ));
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_typeConverter() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ RawDao.UserNameAndBirthday result = mRawDao.getUserAndBirthday(
+ new SimpleSQLiteQuery("SELECT mName, mBirthday FROM user LIMIT 1"));
+ assertThat(result.name, is(user.getName()));
+ assertThat(result.birthday, is(user.getBirthday()));
+ }
+
+ @Test
+ public void embedded_nullField() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet(
+ new SimpleSQLiteQuery("SELECT * FROM User LIMIT 1"));
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void embedded_list() {
+ User[] users = TestUtil.createUsersArray(3, 5);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mUserDao.insertAll(users);
+ mPetDao.insertAll(pets);
+ List<UserAndPet> received = mRawDao.getUserAndPetList(
+ new SimpleSQLiteQuery(
+ "SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
+ + " ORDER BY mId ASC, mPetId ASC"));
+ assertThat(received.size(), is(3));
+ // row 0
+ assertThat(received.get(0).getUser(), is(users[0]));
+ assertThat(received.get(0).getPet(), is(pets[0]));
+ // row 1
+ assertThat(received.get(1).getUser(), is(users[0]));
+ assertThat(received.get(1).getPet(), is(pets[1]));
+ // row 2
+ assertThat(received.get(2).getUser(), is(users[1]));
+ assertThat(received.get(2).getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void count() {
+ mUserDao.insertAll(TestUtil.createUsersArray(3, 5, 7, 10));
+ int count = mRawDao.count(new SimpleSQLiteQuery("SELECT COUNT(*) FROM User"));
+ assertThat(count, is(4));
+ }
+
+ @Test
+ public void embedded_liveData() throws TimeoutException, InterruptedException {
+ LiveData<List<UserAndPet>> liveData = mRawDao.getUserAndPetListObservable(
+ new SimpleSQLiteQuery("SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
+ + " ORDER BY mId ASC, mPetId ASC"));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> liveData.observeForever(user -> {
+ })
+ );
+ drain();
+ assertThat(liveData.getValue(), is(emptyList()));
+
+ User[] users = TestUtil.createUsersArray(3, 5);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mUserDao.insertAll(users);
+ drain();
+ List<UserAndPet> justUsers = liveData.getValue();
+ //noinspection ConstantConditions
+ assertThat(justUsers.size(), is(2));
+ assertThat(justUsers.get(0).getUser(), is(users[0]));
+ assertThat(justUsers.get(1).getUser(), is(users[1]));
+ assertThat(justUsers.get(0).getPet(), is(nullValue()));
+ assertThat(justUsers.get(1).getPet(), is(nullValue()));
+
+ mPetDao.insertAll(pets);
+ drain();
+ List<UserAndPet> allItems = liveData.getValue();
+ //noinspection ConstantConditions
+ assertThat(allItems.size(), is(3));
+ // row 0
+ assertThat(allItems.get(0).getUser(), is(users[0]));
+ assertThat(allItems.get(0).getPet(), is(pets[0]));
+ // row 1
+ assertThat(allItems.get(1).getUser(), is(users[0]));
+ assertThat(allItems.get(1).getPet(), is(pets[1]));
+ // row 2
+ assertThat(allItems.get(2).getUser(), is(users[1]));
+ assertThat(allItems.get(2).getPet(), is(nullValue()));
+
+ mDatabase.clearAllTables();
+ drain();
+ assertThat(liveData.getValue(), is(emptyList()));
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Test
+ public void relation_liveData() throws TimeoutException, InterruptedException {
+ LiveData<UserAndAllPets> liveData = mRawDao
+ .getUserAndAllPetsObservable(
+ new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3}));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> liveData.observeForever(user -> {
+ })
+ );
+ drain();
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ assertThat(liveData.getValue().user, is(user));
+ assertThat(liveData.getValue().pets, is(emptyList()));
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 5);
+ mPetDao.insertAll(pets);
+ drain();
+ assertThat(liveData.getValue().user, is(user));
+ assertThat(liveData.getValue().pets, is(Arrays.asList(pets)));
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RelationWithReservedKeywordTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RelationWithReservedKeywordTest.java
new file mode 100644
index 0000000..774b879
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RelationWithReservedKeywordTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Collections.singletonList;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Database;
+import androidx.room.Embedded;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.Insert;
+import androidx.room.PrimaryKey;
+import androidx.room.Query;
+import androidx.room.Relation;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.Transaction;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RelationWithReservedKeywordTest {
+ private MyDatabase mDb;
+
+ @Before
+ public void initDb() {
+ mDb = Room.inMemoryDatabaseBuilder(
+ InstrumentationRegistry.getTargetContext(),
+ MyDatabase.class).build();
+ }
+
+ @Test
+ public void loadRelation() {
+ Category category = new Category(1, "cat1");
+ mDb.getDao().insert(category);
+ Topic topic = new Topic(2, 1, "foo");
+ mDb.getDao().insert(topic);
+ List<CategoryWithTopics> categoryWithTopics = mDb.getDao().loadAll();
+ assertThat(categoryWithTopics.size(), is(1));
+ assertThat(categoryWithTopics.get(0).category, is(category));
+ assertThat(categoryWithTopics.get(0).topics, is(singletonList(topic)));
+ }
+
+ @Entity(tableName = "categories")
+ static class Category {
+
+ @PrimaryKey(autoGenerate = true)
+ public final long id;
+
+ public final String name;
+
+ Category(long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Category category = (Category) o;
+ //noinspection SimplifiableIfStatement
+ if (id != category.id) return false;
+ return name != null ? name.equals(category.name) : category.name == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (id ^ (id >>> 32));
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ return result;
+ }
+ }
+
+ @Dao
+ interface MyDao {
+ @Transaction
+ @Query("SELECT * FROM categories")
+ List<CategoryWithTopics> loadAll();
+
+ @Insert
+ void insert(Category... categories);
+
+ @Insert
+ void insert(Topic... topics);
+ }
+
+ @Database(
+ entities = {Category.class, Topic.class},
+ version = 1,
+ exportSchema = false)
+ abstract static class MyDatabase extends RoomDatabase {
+ abstract MyDao getDao();
+ }
+
+
+ @SuppressWarnings("WeakerAccess")
+ static class CategoryWithTopics {
+ @Embedded
+ public Category category;
+
+ @Relation(
+ parentColumn = "id",
+ entityColumn = "category_id",
+ entity = Topic.class)
+ public List<Topic> topics;
+ }
+
+ @Entity(
+ tableName = "topics",
+ foreignKeys = @ForeignKey(
+ entity = Category.class,
+ parentColumns = "id",
+ childColumns = "category_id",
+ onDelete = ForeignKey.CASCADE),
+ indices = @Index("category_id"))
+ static class Topic {
+
+ @PrimaryKey(autoGenerate = true)
+ public final long id;
+
+ @ColumnInfo(name = "category_id")
+ public final long categoryId;
+
+ public final String to;
+
+ Topic(long id, long categoryId, String to) {
+ this.id = id;
+ this.categoryId = categoryId;
+ this.to = to;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Topic topic = (Topic) o;
+ if (id != topic.id) return false;
+ //noinspection SimplifiableIfStatement
+ if (categoryId != topic.categoryId) return false;
+ return to != null ? to.equals(topic.to) : topic.to == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (id ^ (id >>> 32));
+ result = 31 * result + (int) (categoryId ^ (categoryId >>> 32));
+ result = 31 * result + (to != null ? to.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RxJava2Test.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RxJava2Test.java
new file mode 100644
index 0000000..9878cd2
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RxJava2Test.java
@@ -0,0 +1,332 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.arch.core.executor.TaskExecutor;
+import androidx.room.EmptyResultSetException;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RxJava2Test extends TestDatabaseTest {
+
+ private TestScheduler mTestScheduler;
+
+ @Before
+ public void setupSchedulers() {
+ mTestScheduler = new TestScheduler();
+ mTestScheduler.start();
+ ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
+ @Override
+ public void executeOnDiskIO(Runnable runnable) {
+ mTestScheduler.scheduleDirect(runnable);
+ }
+
+ @Override
+ public void postToMainThread(Runnable runnable) {
+ Assert.fail("no main thread in this test");
+ }
+
+ @Override
+ public boolean isMainThread() {
+ return false;
+ }
+ });
+ }
+
+ @After
+ public void clearSchedulers() {
+ mTestScheduler.shutdown();
+ ArchTaskExecutor.getInstance().setDelegate(null);
+ }
+
+ private void drain() throws InterruptedException {
+ mTestScheduler.triggerActions();
+ }
+
+ @Test
+ public void maybeUser_Empty() throws InterruptedException {
+ TestObserver<User> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.maybeUserById(3).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ testObserver.assertNoValues();
+ disposable.dispose();
+ }
+
+ @Test
+ public void maybeUser_WithData() throws InterruptedException {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ TestObserver<User> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.maybeUserById(3).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ testObserver.assertValue(user);
+
+ disposable.dispose();
+ }
+
+ @Test
+ public void maybeUsers_EmptyList() throws InterruptedException {
+ TestObserver<List<User>> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.maybeUsersByIds(3, 5, 7).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ testObserver.assertValue(Collections.<User>emptyList());
+ disposable.dispose();
+ }
+
+ @Test
+ public void maybeUsers_WithValue() throws InterruptedException {
+ User[] users = TestUtil.createUsersArray(3, 5);
+ mUserDao.insertAll(users);
+ TestObserver<List<User>> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.maybeUsersByIds(3, 5, 7).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ // since this is a clean db, it is ok to rely on the order for the test.
+ testObserver.assertValue(Arrays.asList(users));
+ disposable.dispose();
+ }
+
+ @Test
+ public void singleUser_Empty() throws InterruptedException {
+ TestObserver<User> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.singleUserById(3).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ // figure out which error we should dispatch
+ testObserver.assertError(EmptyResultSetException.class);
+ testObserver.assertNoValues();
+ disposable.dispose();
+ }
+
+ @Test
+ public void singleUser_WithData() throws InterruptedException {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ TestObserver<User> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.singleUserById(3).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ testObserver.assertValue(user);
+
+ disposable.dispose();
+ }
+
+ @Test
+ public void singleUsers_EmptyList() throws InterruptedException {
+ TestObserver<List<User>> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.singleUsersByIds(3, 5, 7).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ testObserver.assertValue(Collections.<User>emptyList());
+ disposable.dispose();
+ }
+
+ @Test
+ public void singleUsers_WithValue() throws InterruptedException {
+ User[] users = TestUtil.createUsersArray(3, 5);
+ mUserDao.insertAll(users);
+ TestObserver<List<User>> testObserver = new TestObserver<>();
+ Disposable disposable = mUserDao.singleUsersByIds(3, 5, 7).observeOn(mTestScheduler)
+ .subscribeWith(testObserver);
+ drain();
+ testObserver.assertComplete();
+ // since this is a clean db, it is ok to rely on the order for the test.
+ testObserver.assertValue(Arrays.asList(users));
+ disposable.dispose();
+ }
+
+ @Test
+ public void observeOnce() throws InterruptedException {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ TestSubscriber<User> consumer = new TestSubscriber<>();
+ Disposable disposable = mUserDao.flowableUserById(3).subscribeWith(consumer);
+ drain();
+ consumer.assertValue(user);
+ disposable.dispose();
+ }
+
+ @Test
+ public void observeChangeAndDispose() throws InterruptedException {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ TestSubscriber<User> consumer = new TestSubscriber<>();
+ Disposable disposable = mUserDao.flowableUserById(3).observeOn(mTestScheduler)
+ .subscribeWith(consumer);
+ drain();
+ assertThat(consumer.values().get(0), is(user));
+ user.setName("rxy");
+ mUserDao.insertOrReplace(user);
+ drain();
+ User next = consumer.values().get(1);
+ assertThat(next, is(user));
+ disposable.dispose();
+ user.setName("foo");
+ mUserDao.insertOrReplace(user);
+ drain();
+ assertThat(consumer.valueCount(), is(2));
+ }
+
+ @Test
+ @MediumTest
+ public void observeEmpty() throws InterruptedException {
+ TestSubscriber<User> consumer = new TestSubscriber<>();
+ Disposable disposable = mUserDao.flowableUserById(3).observeOn(mTestScheduler)
+ .subscribeWith(consumer);
+ drain();
+ consumer.assertNoValues();
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ assertThat(consumer.values().get(0), is(user));
+ disposable.dispose();
+ user.setAge(88);
+ mUserDao.insertOrReplace(user);
+ drain();
+ assertThat(consumer.valueCount(), is(1));
+ }
+
+ @Test
+ public void flowableCountUsers() throws InterruptedException {
+ TestSubscriber<Integer> consumer = new TestSubscriber<>();
+ mUserDao.flowableCountUsers()
+ .observeOn(mTestScheduler)
+ .subscribe(consumer);
+ drain();
+ assertThat(consumer.values().get(0), is(0));
+ mUserDao.insertAll(TestUtil.createUsersArray(1, 3, 4, 6));
+ drain();
+ assertThat(consumer.values().get(1), is(4));
+ mUserDao.deleteByUids(3, 7);
+ drain();
+ assertThat(consumer.values().get(2), is(3));
+ mUserDao.deleteByUids(101);
+ drain();
+ assertThat(consumer.valueCount(), is(3));
+ }
+
+ @Test
+ @MediumTest
+ public void publisherCountUsers() throws InterruptedException {
+ TestSubscriber<Integer> subscriber = new TestSubscriber<>();
+ mUserDao.publisherCountUsers().subscribe(subscriber);
+ drain();
+ subscriber.assertSubscribed();
+ subscriber.request(2);
+ drain();
+ subscriber.assertValue(0);
+ mUserDao.insert(TestUtil.createUser(2));
+ drain();
+ subscriber.assertValues(0, 1);
+ 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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java
new file mode 100644
index 0000000..992d8f8
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.vo.User;
+
+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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java
new file mode 100644
index 0000000..0207190
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -0,0 +1,612 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.BlobEntityDao;
+import androidx.room.integration.testapp.dao.PetDao;
+import androidx.room.integration.testapp.dao.ProductDao;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.dao.UserPetDao;
+import androidx.room.integration.testapp.vo.BlobEntity;
+import androidx.room.integration.testapp.vo.Day;
+import androidx.room.integration.testapp.vo.NameAndLastName;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.Product;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.integration.testapp.vo.UserAndAllPets;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SimpleEntityReadWriteTest {
+ private UserDao mUserDao;
+ private BlobEntityDao mBlobEntityDao;
+ private PetDao mPetDao;
+ private UserPetDao mUserPetDao;
+ private ProductDao mProductDao;
+
+ @Before
+ public void createDb() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ mUserDao = db.getUserDao();
+ mPetDao = db.getPetDao();
+ mUserPetDao = db.getUserPetDao();
+ mBlobEntityDao = db.getBlobEntityDao();
+ mProductDao = db.getProductDao();
+ }
+
+ @Test
+ public void writeUserAndReadInList() throws Exception {
+ User user = TestUtil.createUser(3);
+ user.setName("george");
+ mUserDao.insert(user);
+ List<User> byName = mUserDao.findUsersByName("george");
+ assertThat(byName.get(0), equalTo(user));
+ }
+
+ @Test
+ public void insertNull() throws Exception {
+ @SuppressWarnings("ConstantConditions")
+ Product product = new Product(1, null);
+ Throwable throwable = null;
+ try {
+ mProductDao.insert(product);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull("Was expecting an exception", throwable);
+ assertThat(throwable, instanceOf(SQLiteConstraintException.class));
+ }
+
+ @Test
+ public void insertDifferentEntities() throws Exception {
+ User user1 = TestUtil.createUser(3);
+ user1.setName("george");
+ Pet pet = TestUtil.createPet(1);
+ pet.setUserId(3);
+ pet.setName("a");
+ mUserPetDao.insertUserAndPet(user1, pet);
+ assertThat(mUserDao.count(), is(1));
+ List<UserAndAllPets> inserted = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(inserted, hasSize(1));
+ assertThat(inserted.get(0).user.getId(), is(3));
+ assertThat(inserted.get(0).user.getName(), is(equalTo("george")));
+ assertThat(inserted.get(0).pets, hasSize(1));
+ assertThat(inserted.get(0).pets.get(0).getPetId(), is(1));
+ assertThat(inserted.get(0).pets.get(0).getName(), is("a"));
+ assertThat(inserted.get(0).pets.get(0).getUserId(), is(3));
+ pet.setName("b");
+ mUserPetDao.updateUsersAndPets(new User[]{user1}, new Pet[]{pet});
+ List<UserAndAllPets> updated = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(updated, hasSize(1));
+ assertThat(updated.get(0).pets, hasSize(1));
+ assertThat(updated.get(0).pets.get(0).getName(), is("b"));
+ User user2 = TestUtil.createUser(5);
+ user2.setName("chet");
+ mUserDao.insert(user2);
+ assertThat(mUserDao.count(), is(2));
+ mUserPetDao.delete2UsersAndPets(user1, user2, new Pet[]{pet});
+ List<UserAndAllPets> deleted = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(deleted, hasSize(0));
+ }
+
+ @Test
+ public void insertDifferentEntities_transaction() throws Exception {
+ Pet pet = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet);
+ assertThat(mPetDao.count(), is(1));
+ User user = TestUtil.createUser(3);
+ try {
+ mUserPetDao.insertUserAndPet(user, pet);
+ fail("Exception expected");
+ } catch (SQLiteConstraintException ignored) {
+ }
+ assertThat(mUserDao.count(), is(0));
+ assertThat(mPetDao.count(), is(1));
+ }
+
+ @Test
+ public void throwExceptionOnConflict() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+
+ User user2 = TestUtil.createUser(3);
+ try {
+ mUserDao.insert(user2);
+ throw new AssertionError("didn't throw in conflicting insertion");
+ } catch (SQLiteException ignored) {
+ }
+ }
+
+ @Test
+ public void replaceOnConflict() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+
+ User user2 = TestUtil.createUser(3);
+ mUserDao.insertOrReplace(user2);
+
+ assertThat(mUserDao.load(3), equalTo(user2));
+ assertThat(mUserDao.load(3), not(equalTo(user)));
+ }
+
+ @Test
+ public void updateSimple() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ user.setName("i am an updated name");
+ assertThat(mUserDao.update(user), is(1));
+ assertThat(mUserDao.load(user.getId()), equalTo(user));
+ }
+
+ @Test
+ public void updateNonExisting() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ User user2 = TestUtil.createUser(4);
+ assertThat(mUserDao.update(user2), is(0));
+ }
+
+ @Test
+ public void updateList() {
+ List<User> users = TestUtil.createUsersList(3, 4, 5);
+ mUserDao.insertAll(users.toArray(new User[3]));
+ for (User user : users) {
+ user.setName("name " + user.getId());
+ }
+ assertThat(mUserDao.updateAll(users), is(3));
+ for (User user : users) {
+ assertThat(mUserDao.load(user.getId()).getName(), is("name " + user.getId()));
+ }
+ }
+
+ @Test
+ public void updateListPartial() {
+ List<User> existingUsers = TestUtil.createUsersList(3, 4, 5);
+ mUserDao.insertAll(existingUsers.toArray(new User[3]));
+ for (User user : existingUsers) {
+ user.setName("name " + user.getId());
+ }
+ List<User> allUsers = TestUtil.createUsersList(7, 8, 9);
+ allUsers.addAll(existingUsers);
+ assertThat(mUserDao.updateAll(allUsers), is(3));
+ for (User user : existingUsers) {
+ assertThat(mUserDao.load(user.getId()).getName(), is("name " + user.getId()));
+ }
+ }
+
+ @Test
+ public void delete() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ assertThat(mUserDao.delete(user), is(1));
+ assertThat(mUserDao.delete(user), is(0));
+ assertThat(mUserDao.load(3), is(nullValue()));
+ }
+
+ @Test
+ public void deleteAll() {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
+ mUserDao.insertAll(users);
+ // there is actually no guarantee for this order by works fine since they are ordered for
+ // the test and it is a new database (no pages to recycle etc)
+ assertThat(mUserDao.loadByIds(3, 5, 7, 9), is(users));
+ int deleteCount = mUserDao.deleteAll(new User[]{users[0], users[3],
+ TestUtil.createUser(9)});
+ assertThat(deleteCount, is(2));
+ assertThat(mUserDao.loadByIds(3, 5, 7, 9), is(new User[]{users[1], users[2]}));
+ }
+
+ @Test
+ public void deleteEverything() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ assertThat(mUserDao.count(), is(1));
+ int count = mUserDao.deleteEverything();
+ assertThat(count, is(1));
+ assertThat(mUserDao.count(), is(0));
+ }
+
+ @Test
+ public void findByBoolean() {
+ User user1 = TestUtil.createUser(3);
+ user1.setAdmin(true);
+ User user2 = TestUtil.createUser(5);
+ user2.setAdmin(false);
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ assertThat(mUserDao.findByAdmin(true), is(Arrays.asList(user1)));
+ assertThat(mUserDao.findByAdmin(false), is(Arrays.asList(user2)));
+ }
+
+ @Test
+ public void returnBoolean() {
+ User user1 = TestUtil.createUser(1);
+ User user2 = TestUtil.createUser(2);
+ user1.setAdmin(true);
+ user2.setAdmin(false);
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ assertThat(mUserDao.isAdmin(1), is(true));
+ assertThat(mUserDao.isAdmin(2), is(false));
+ }
+
+ @Test
+ public void findByCollateNoCase() {
+ User user = TestUtil.createUser(3);
+ user.setCustomField("abc");
+ mUserDao.insert(user);
+ List<User> users = mUserDao.findByCustomField("ABC");
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0).getId(), is(3));
+ }
+
+ @Test
+ public void deleteByAge() {
+ User user1 = TestUtil.createUser(3);
+ user1.setAge(30);
+ User user2 = TestUtil.createUser(5);
+ user2.setAge(45);
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ assertThat(mUserDao.deleteAgeGreaterThan(60), is(0));
+ assertThat(mUserDao.deleteAgeGreaterThan(45), is(0));
+ assertThat(mUserDao.deleteAgeGreaterThan(35), is(1));
+ assertThat(mUserDao.loadByIds(3, 5), is(new User[]{user1}));
+ }
+
+ @Test
+ public void deleteByAgeRange() {
+ User user1 = TestUtil.createUser(3);
+ user1.setAge(30);
+ User user2 = TestUtil.createUser(5);
+ user2.setAge(45);
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ assertThat(mUserDao.deleteByAgeRange(35, 40), is(0));
+ assertThat(mUserDao.deleteByAgeRange(25, 30), is(1));
+ assertThat(mUserDao.loadByIds(3, 5), is(new User[]{user2}));
+ }
+
+ @Test
+ public void deleteByUIds() {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 9, 11);
+ mUserDao.insertAll(users);
+ assertThat(mUserDao.deleteByUids(2, 4, 6), is(0));
+ assertThat(mUserDao.deleteByUids(3, 11), is(2));
+ assertThat(mUserDao.loadByIds(3, 5, 7, 9, 11), is(new User[]{
+ users[1], users[2], users[3]
+ }));
+ }
+
+ @Test
+ public void updateNameById() {
+ User[] usersArray = TestUtil.createUsersArray(3, 5, 7);
+ mUserDao.insertAll(usersArray);
+ assertThat("test sanity", usersArray[1].getName(), not(equalTo("updated name")));
+ int changed = mUserDao.updateById(5, "updated name");
+ assertThat(changed, is(1));
+ assertThat(mUserDao.load(5).getName(), is("updated name"));
+ }
+
+ @Test
+ public void incrementIds() {
+ User[] usersArr = TestUtil.createUsersArray(2, 4, 6);
+ mUserDao.insertAll(usersArr);
+ mUserDao.incrementIds(1);
+ assertThat(mUserDao.loadIds(), is(Arrays.asList(3, 5, 7)));
+ }
+
+ @Test
+ public void incrementAgeOfAll() {
+ User[] users = TestUtil.createUsersArray(3, 5, 7);
+ users[0].setAge(3);
+ users[1].setAge(5);
+ users[2].setAge(7);
+ mUserDao.insertAll(users);
+ assertThat(mUserDao.count(), is(3));
+ mUserDao.incrementAgeOfAll();
+ for (User user : mUserDao.loadByIds(3, 5, 7)) {
+ assertThat(user.getAge(), is(user.getId() + 1));
+ }
+ }
+
+ @Test
+ public void findByIntQueryParameter() {
+ User user = TestUtil.createUser(1);
+ final String name = "my name";
+ user.setName(name);
+ mUserDao.insert(user);
+ assertThat(mUserDao.findByNameLength(name.length()), is(Collections.singletonList(user)));
+ }
+
+ @Test
+ public void findByIntFieldMatch() {
+ User user = TestUtil.createUser(1);
+ user.setAge(19);
+ mUserDao.insert(user);
+ assertThat(mUserDao.findByAge(19), is(Collections.singletonList(user)));
+ }
+
+ @Test
+ public void customConverterField() {
+ User user = TestUtil.createUser(20);
+ Date theDate = new Date(System.currentTimeMillis() - 200);
+ user.setBirthday(theDate);
+ mUserDao.insert(user);
+ assertThat(mUserDao.findByBirthdayRange(new Date(theDate.getTime() - 100),
+ new Date(theDate.getTime() + 1)).get(0), is(user));
+ assertThat(mUserDao.findByBirthdayRange(new Date(theDate.getTime()),
+ new Date(theDate.getTime() + 1)).size(), is(0));
+ }
+
+ @Test
+ public void renamedField() {
+ User user = TestUtil.createUser(3);
+ user.setCustomField("foo laaa");
+ mUserDao.insertOrReplace(user);
+ User loaded = mUserDao.load(3);
+ assertThat(loaded.getCustomField(), is("foo laaa"));
+ assertThat(loaded, is(user));
+ }
+
+ @Test
+ public void readViaCursor() {
+ User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
+ mUserDao.insertAll(users);
+ Cursor cursor = mUserDao.findUsersAsCursor(3, 5, 9);
+ try {
+ assertThat(cursor.getCount(), is(3));
+ assertThat(cursor.moveToNext(), is(true));
+ assertThat(cursor.getInt(0), is(3));
+ assertThat(cursor.moveToNext(), is(true));
+ assertThat(cursor.getInt(0), is(5));
+ assertThat(cursor.moveToNext(), is(true));
+ assertThat(cursor.getInt(0), is(9));
+ assertThat(cursor.moveToNext(), is(false));
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test
+ public void readDirectWithTypeAdapter() {
+ User user = TestUtil.createUser(3);
+ user.setBirthday(null);
+ mUserDao.insert(user);
+ assertThat(mUserDao.getBirthday(3), is(nullValue()));
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.YEAR, 3);
+ Date birthday = calendar.getTime();
+ user.setBirthday(birthday);
+
+ mUserDao.update(user);
+ assertThat(mUserDao.getBirthday(3), is(birthday));
+ }
+
+ @Test
+ public void emptyInQuery() {
+ User[] users = mUserDao.loadByIds();
+ assertThat(users, is(new User[0]));
+ }
+
+ @Test
+ public void blob() {
+ BlobEntity a = new BlobEntity(1, "abc".getBytes());
+ BlobEntity b = new BlobEntity(2, "def".getBytes());
+ mBlobEntityDao.insert(a);
+ mBlobEntityDao.insert(b);
+ List<BlobEntity> list = mBlobEntityDao.selectAll();
+ assertThat(list, hasSize(2));
+ mBlobEntityDao.updateContent(2, "ghi".getBytes());
+ assertThat(mBlobEntityDao.getContent(2), is(equalTo("ghi".getBytes())));
+ }
+
+ @Test
+ public void transactionByRunnable() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(5);
+ mUserDao.insertBothByRunnable(a, b);
+ assertThat(mUserDao.count(), is(2));
+ }
+
+ @Test
+ public void transactionByRunnable_failure() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(3);
+ boolean caught = false;
+ try {
+ mUserDao.insertBothByRunnable(a, b);
+ } catch (SQLiteConstraintException e) {
+ caught = true;
+ }
+ assertTrue("SQLiteConstraintException expected", caught);
+ assertThat(mUserDao.count(), is(0));
+ }
+
+ @Test
+ public void transactionByCallable() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(5);
+ int count = mUserDao.insertBothByCallable(a, b);
+ assertThat(mUserDao.count(), is(2));
+ assertThat(count, is(2));
+ }
+
+ @Test
+ public void transactionByCallable_failure() {
+ User a = TestUtil.createUser(3);
+ User b = TestUtil.createUser(3);
+ boolean caught = false;
+ try {
+ mUserDao.insertBothByCallable(a, b);
+ } catch (SQLiteConstraintException e) {
+ caught = true;
+ }
+ assertTrue("SQLiteConstraintException expected", caught);
+ assertThat(mUserDao.count(), is(0));
+ }
+
+ @Test
+ public void transactionByDefaultImplementation() {
+ Pet pet1 = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet1);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ Pet pet2 = TestUtil.createPet(2);
+ mPetDao.deleteAndInsert(pet1, pet2, false);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(2));
+ }
+
+ @Test
+ public void transactionByDefaultImplementation_failure() {
+ Pet pet1 = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet1);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ Pet pet2 = TestUtil.createPet(2);
+ Throwable throwable = null;
+ try {
+ mPetDao.deleteAndInsert(pet1, pet2, true);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull("Was expecting an exception", throwable);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ }
+
+ @Test
+ public void multipleInParamsFollowedByASingleParam_delete() {
+ User user = TestUtil.createUser(3);
+ user.setAge(30);
+ mUserDao.insert(user);
+ assertThat(mUserDao.deleteByAgeAndIds(20, Arrays.asList(3, 5)), is(0));
+ assertThat(mUserDao.count(), is(1));
+ assertThat(mUserDao.deleteByAgeAndIds(30, Arrays.asList(3, 5)), is(1));
+ assertThat(mUserDao.count(), is(0));
+ }
+
+ @Test
+ public void multipleInParamsFollowedByASingleParam_update() {
+ User user = TestUtil.createUser(3);
+ user.setAge(30);
+ user.setWeight(10f);
+ mUserDao.insert(user);
+ assertThat(mUserDao.updateByAgeAndIds(3f, 20, Arrays.asList(3, 5)), is(0));
+ assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(10f));
+ 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));
+ }
+
+ @Test
+ public void tablePrefix_simpleSelect() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result = mUserDao.getNameAndLastName(3);
+ assertThat(result.getName(), is(user.getName()));
+ assertThat(result.getLastName(), is(user.getLastName()));
+ }
+
+ @Test
+ public void enumSet_simpleLoad() {
+ User a = TestUtil.createUser(3);
+ Set<Day> expected = toSet(Day.MONDAY, Day.TUESDAY);
+ a.setWorkDays(expected);
+ mUserDao.insert(a);
+ User loaded = mUserDao.load(3);
+ assertThat(loaded.getWorkDays(), is(expected));
+ }
+
+ @Test
+ public void enumSet_query() {
+ User user1 = TestUtil.createUser(3);
+ user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
+ User user2 = TestUtil.createUser(5);
+ user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ List<User> empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
+ assertThat(empty.size(), is(0));
+ List<User> friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
+ assertThat(friday, is(Arrays.asList(user1)));
+ List<User> monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
+ assertThat(monday, is(Arrays.asList(user1, user2)));
+
+ }
+
+ private Set<Day> toSet(Day... days) {
+ return new HashSet<>(Arrays.asList(days));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestDatabaseTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestDatabaseTest.java
new file mode 100644
index 0000000..56a0cbf
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestDatabaseTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+
+import androidx.room.Room;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.FunnyNamedDao;
+import androidx.room.integration.testapp.dao.PetCoupleDao;
+import androidx.room.integration.testapp.dao.PetDao;
+import androidx.room.integration.testapp.dao.RawDao;
+import androidx.room.integration.testapp.dao.SchoolDao;
+import androidx.room.integration.testapp.dao.SpecificDogDao;
+import androidx.room.integration.testapp.dao.ToyDao;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.dao.UserPetDao;
+import androidx.room.integration.testapp.dao.WithClauseDao;
+
+import org.junit.Before;
+
+@SuppressWarnings("WeakerAccess")
+public abstract class TestDatabaseTest {
+ protected TestDatabase mDatabase;
+ protected UserDao mUserDao;
+ protected PetDao mPetDao;
+ protected UserPetDao mUserPetDao;
+ protected SchoolDao mSchoolDao;
+ protected PetCoupleDao mPetCoupleDao;
+ protected ToyDao mToyDao;
+ protected SpecificDogDao mSpecificDogDao;
+ protected WithClauseDao mWithClauseDao;
+ protected FunnyNamedDao mFunnyNamedDao;
+ protected RawDao mRawDao;
+
+ @Before
+ public void createDb() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ mDatabase = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ mUserDao = mDatabase.getUserDao();
+ mPetDao = mDatabase.getPetDao();
+ mUserPetDao = mDatabase.getUserPetDao();
+ mSchoolDao = mDatabase.getSchoolDao();
+ mPetCoupleDao = mDatabase.getPetCoupleDao();
+ mToyDao = mDatabase.getToyDao();
+ mSpecificDogDao = mDatabase.getSpecificDogDao();
+ mWithClauseDao = mDatabase.getWithClauseDao();
+ mFunnyNamedDao = mDatabase.getFunnyNamedDao();
+ mRawDao = mDatabase.getRawDao();
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestUtil.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestUtil.java
new file mode 100644
index 0000000..b3d5e24
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestUtil.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp.test;
+
+import androidx.room.integration.testapp.vo.Address;
+import androidx.room.integration.testapp.vo.Coordinates;
+import androidx.room.integration.testapp.vo.Pet;
+import androidx.room.integration.testapp.vo.School;
+import androidx.room.integration.testapp.vo.Toy;
+import androidx.room.integration.testapp.vo.User;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+public class TestUtil {
+ public static User[] createUsersArray(int... ids) {
+ User[] result = new User[ids.length];
+ for (int i = 0; i < ids.length; i++) {
+ result[i] = createUser(ids[i]);
+ }
+ return result;
+ }
+
+ public static List<User> createUsersList(int... ids) {
+ List<User> result = new ArrayList<>();
+ for (int id : ids) {
+ result.add(createUser(id));
+ }
+ return result;
+ }
+
+ public static User createUser(int id) {
+ User user = new User();
+ user.setId(id);
+ user.setName(UUID.randomUUID().toString());
+ user.setLastName(UUID.randomUUID().toString());
+ user.setAge((int) (10 + Math.random() * 50));
+ user.setCustomField(UUID.randomUUID().toString());
+ user.setBirthday(new Date());
+ return user;
+ }
+
+ public static Pet createPet(int id) {
+ Pet pet = new Pet();
+ pet.setPetId(id);
+ pet.setName(UUID.randomUUID().toString());
+ pet.setAdoptionDate(new Date());
+ return pet;
+ }
+
+ public static Toy createToyForPet(Pet pet, int toyId) {
+ Toy toy = new Toy();
+ toy.setName("toy " + toyId);
+ toy.setId(toyId);
+ toy.setPetId(pet.getPetId());
+ return toy;
+ }
+
+ public static Pet[] createPetsForUser(int uid, int petStartId, int count) {
+ Pet[] pets = new Pet[count];
+ for (int i = 0; i < count; i++) {
+ Pet pet = createPet(petStartId++);
+ pet.setUserId(uid);
+ pets[i] = pet;
+ }
+ return pets;
+ }
+
+ public static School createSchool(int id, int managerId) {
+ School school = new School();
+ school.setId(id);
+ school.setName(UUID.randomUUID().toString());
+ school.setManager(createUser(managerId));
+ school.setAddress(createAddress());
+ return school;
+ }
+
+ private static Address createAddress() {
+ Address address = new Address();
+ address.setCoordinates(createCoordinates());
+ address.setPostCode((int) (Math.random() * 1000 + 1000));
+ address.setState(UUID.randomUUID().toString().substring(0, 2));
+ address.setStreet(UUID.randomUUID().toString());
+ return address;
+ }
+
+ private static Coordinates createCoordinates() {
+ Coordinates coordinates = new Coordinates();
+ coordinates.lat = Math.random();
+ coordinates.lng = Math.random();
+ return coordinates;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WithClauseTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WithClauseTest.java
new file mode 100644
index 0000000..85f7697
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WithClauseTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.room.integration.testapp.vo.User;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+public class WithClauseTest extends TestDatabaseTest{
+ @Test
+ public void noSourceOfData() {
+ @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+ List<Integer> expected = Arrays.asList(1);
+ List<Integer> actual = mWithClauseDao.getFactorials(0);
+ assertThat(expected, is(actual));
+
+ expected = Arrays.asList(1, 1, 2, 6, 24);
+ actual = mWithClauseDao.getFactorials(4);
+ assertThat(expected, is(actual));
+ }
+
+ @Test
+ public void sourceOfData() {
+ List<String> expected = new ArrayList<>();
+ List<String> actual = mWithClauseDao.getUsersWithFactorialIds(0);
+ assertThat(expected, is(actual));
+
+ User user = new User();
+ user.setId(0);
+ user.setName("Zero");
+ mUserDao.insert(user);
+ actual = mWithClauseDao.getUsersWithFactorialIds(0);
+ assertThat(expected, is(actual));
+
+ user = new User();
+ user.setId(1);
+ user.setName("One");
+ mUserDao.insert(user);
+ expected.add("One");
+ actual = mWithClauseDao.getUsersWithFactorialIds(0);
+ assertThat(expected, is(actual));
+
+ user = new User();
+ user.setId(6);
+ user.setName("Six");
+ mUserDao.insert(user);
+ actual = mWithClauseDao.getUsersWithFactorialIds(0);
+ assertThat(expected, is(actual));
+
+ actual = mWithClauseDao.getUsersWithFactorialIds(3);
+ expected.add("Six");
+ assertThat(expected, is(actual));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
new file mode 100644
index 0000000..c41d5b2
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2018 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 androidx.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalToIgnoringCase;
+import static org.hamcrest.Matchers.hasSize;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.room.InvalidationTracker;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.vo.User;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+@SdkSuppress(minSdkVersion = 16)
+public class WriteAheadLoggingTest {
+
+ private static final String DATABASE_NAME = "wal.db";
+ private TestDatabase mDatabase;
+
+ @Before
+ public void openDatabase() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ context.deleteDatabase(DATABASE_NAME);
+ mDatabase = Room.databaseBuilder(context, TestDatabase.class, DATABASE_NAME)
+ .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
+ .build();
+ }
+
+ @After
+ public void closeDatabase() {
+ mDatabase.close();
+ Context context = InstrumentationRegistry.getTargetContext();
+ context.deleteDatabase(DATABASE_NAME);
+ }
+
+ @Test
+ public void checkJournalMode() {
+ Cursor c = null;
+ try {
+ SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
+ c = db.query("PRAGMA journal_mode");
+ c.moveToFirst();
+ String journalMode = c.getString(0);
+ assertThat(journalMode, is(equalToIgnoringCase("wal")));
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ @Test
+ public void disableWal() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ mDatabase.close();
+ mDatabase = Room.databaseBuilder(context, TestDatabase.class, DATABASE_NAME)
+ .setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
+ .build();
+ Cursor c = null;
+ try {
+ SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
+ c = db.query("PRAGMA journal_mode");
+ c.moveToFirst();
+ String journalMode = c.getString(0);
+ assertThat(journalMode, is(not(equalToIgnoringCase("wal"))));
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ @Test
+ public void observeLiveData() {
+ UserDao dao = mDatabase.getUserDao();
+ LiveData<User> user1 = dao.liveUserById(1);
+ Observer<User> observer = startObserver(user1);
+ dao.insert(TestUtil.createUser(1));
+ verify(observer, timeout(3000).atLeastOnce())
+ .onChanged(argThat(user -> user != null && user.getId() == 1));
+ stopObserver(user1, observer);
+ }
+
+ @Test
+ public void observeLiveDataWithTransaction() {
+ UserDao dao = mDatabase.getUserDao();
+ LiveData<User> user1 = dao.liveUserByIdInTransaction(1);
+ Observer<User> observer = startObserver(user1);
+ dao.insert(TestUtil.createUser(1));
+ verify(observer, timeout(3000).atLeastOnce())
+ .onChanged(argThat(user -> user != null && user.getId() == 1));
+ stopObserver(user1, observer);
+ }
+
+ @Test
+ public void parallelWrites() throws InterruptedException, ExecutionException {
+ int numberOfThreads = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
+ ArrayList<Future<Boolean>> futures = new ArrayList<>();
+ for (int i = 0; i < numberOfThreads; i++) {
+ final int id = i + 1;
+ futures.add(i, executor.submit(() -> {
+ User user = TestUtil.createUser(id);
+ user.setName("user" + id);
+ mDatabase.getUserDao().insert(user);
+ return true;
+ }));
+ }
+ LiveData<List<User>> usersList = mDatabase.getUserDao().liveUsersListByName("user");
+ Observer<List<User>> observer = startObserver(usersList);
+ for (Future future : futures) {
+ assertThat(future.get(), is(true));
+ }
+ verify(observer, timeout(3000).atLeastOnce())
+ .onChanged(argThat(users -> users != null && users.size() == numberOfThreads));
+ stopObserver(usersList, observer);
+ }
+
+ @Test
+ public void readInBackground() throws InterruptedException, ExecutionException {
+ final UserDao dao = mDatabase.getUserDao();
+ final User user1 = TestUtil.createUser(1);
+ dao.insert(user1);
+ try {
+ mDatabase.beginTransaction();
+ dao.delete(user1);
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<?> future = executor.submit(() ->
+ assertThat(dao.load(1), is(equalTo(user1))));
+ future.get();
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+ assertThat(dao.count(), is(0));
+ }
+
+ @Test
+ @LargeTest
+ public void observeInvalidationInBackground() throws InterruptedException, ExecutionException {
+ final UserDao dao = mDatabase.getUserDao();
+ final User user1 = TestUtil.createUser(1);
+ final CountDownLatch observerRegistered = new CountDownLatch(1);
+ final CountDownLatch onInvalidatedCalled = new CountDownLatch(1);
+ dao.insert(user1);
+ Future future;
+ try {
+ mDatabase.beginTransaction();
+ dao.delete(user1);
+ future = Executors.newSingleThreadExecutor().submit(() -> {
+ // Adding this observer will be blocked by the surrounding transaction.
+ mDatabase.getInvalidationTracker().addObserver(
+ new InvalidationTracker.Observer("User") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ onInvalidatedCalled.countDown(); // This should not happen
+ }
+ });
+ observerRegistered.countDown();
+ });
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ assertThat(observerRegistered.getCount(), is(1L));
+ mDatabase.endTransaction();
+ }
+ assertThat(dao.count(), is(0));
+ assertThat(observerRegistered.await(3000, TimeUnit.MILLISECONDS), is(true));
+ future.get();
+ assertThat(onInvalidatedCalled.await(500, TimeUnit.MILLISECONDS), is(false));
+ }
+
+ @Test
+ public void invalidation() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mDatabase.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("User") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ assertThat(tables, hasSize(1));
+ assertThat(tables, hasItem("User"));
+ latch.countDown();
+ }
+ });
+ mDatabase.getUserDao().insert(TestUtil.createUser(1));
+ assertThat(latch.await(3000, TimeUnit.MILLISECONDS), is(true));
+ }
+
+ private static <T> Observer<T> startObserver(LiveData<T> liveData) {
+ @SuppressWarnings("unchecked")
+ Observer<T> observer = mock(Observer.class);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
+ liveData.observeForever(observer));
+ return observer;
+ }
+
+ private static <T> void stopObserver(LiveData<T> liveData, Observer<T> observer) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
+ liveData.removeObserver(observer));
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Address.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Address.java
new file mode 100644
index 0000000..e57cb87
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Address.java
@@ -0,0 +1,90 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Embedded;
+
+public class Address {
+ @ColumnInfo(name = "street")
+ private String mStreet;
+ @ColumnInfo(name = "state")
+ private String mState;
+ @ColumnInfo(name = "post_code")
+ private int mPostCode;
+ @Embedded
+ private Coordinates mCoordinates;
+
+ public String getStreet() {
+ return mStreet;
+ }
+
+ public void setStreet(String street) {
+ mStreet = street;
+ }
+
+ public String getState() {
+ return mState;
+ }
+
+ public void setState(String state) {
+ mState = state;
+ }
+
+ public int getPostCode() {
+ return mPostCode;
+ }
+
+ public void setPostCode(int postCode) {
+ mPostCode = postCode;
+ }
+
+ public Coordinates getCoordinates() {
+ return mCoordinates;
+ }
+
+ public void setCoordinates(Coordinates coordinates) {
+ mCoordinates = coordinates;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Address address = (Address) o;
+
+ if (mPostCode != address.mPostCode) return false;
+ if (mStreet != null ? !mStreet.equals(address.mStreet) : address.mStreet != null) {
+ return false;
+ }
+ if (mState != null ? !mState.equals(address.mState) : address.mState != null) {
+ return false;
+ }
+ return mCoordinates != null ? mCoordinates.equals(address.mCoordinates)
+ : address.mCoordinates == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mStreet != null ? mStreet.hashCode() : 0;
+ result = 31 * result + (mState != null ? mState.hashCode() : 0);
+ result = 31 * result + mPostCode;
+ result = 31 * result + (mCoordinates != null ? mCoordinates.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AvgWeightByAge.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AvgWeightByAge.java
new file mode 100644
index 0000000..fd7d87f
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/AvgWeightByAge.java
@@ -0,0 +1,87 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Ignore;
+
+@SuppressWarnings("unused")
+public class AvgWeightByAge {
+
+ private int mAge;
+
+ @ColumnInfo(name = "AVG(mWeight)")
+ private float mWeight;
+
+ public AvgWeightByAge() {
+ }
+
+ @Ignore
+ public AvgWeightByAge(int age, float weight) {
+ mAge = age;
+ mWeight = weight;
+ }
+
+ public int getAge() {
+ return mAge;
+ }
+
+ public void setAge(int age) {
+ mAge = age;
+ }
+
+ public float getWeight() {
+ return mWeight;
+ }
+
+ public void setWeight(float weight) {
+ mWeight = weight;
+ }
+
+ @Override
+ public String toString() {
+ return "AvgWeightByAge{"
+ + "mAge=" + mAge
+ + ", mWeight=" + mWeight
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AvgWeightByAge that = (AvgWeightByAge) o;
+
+ //noinspection SimplifiableIfStatement
+ if (mAge != that.mAge) {
+ return false;
+ }
+ return Float.compare(that.mWeight, mWeight) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mAge;
+ result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/BlobEntity.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/BlobEntity.java
new file mode 100644
index 0000000..201773a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/BlobEntity.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class BlobEntity {
+ @PrimaryKey
+ public long id;
+ public byte[] content;
+
+ public BlobEntity(long id, byte[] content) {
+ this.id = id;
+ this.content = content;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Coordinates.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Coordinates.java
new file mode 100644
index 0000000..e30342c
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Coordinates.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 androidx.room.integration.testapp.vo;
+
+public class Coordinates {
+ public double lat;
+ public double lng;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Coordinates that = (Coordinates) o;
+
+ if (Double.compare(that.lat, lat) != 0) return false;
+ return Double.compare(that.lng, lng) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ long temp;
+ temp = Double.doubleToLongBits(lat);
+ result = (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(lng);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Day.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Day.java
new file mode 100644
index 0000000..7ec640a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Day.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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 androidx.room.integration.testapp.vo;
+
+public enum Day {
+ MONDAY,
+ TUESDAY,
+ WEDNESDAY,
+ THURSDAY,
+ FRIDAY,
+ SATURDAY,
+ SUNDAY
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/EmbeddedUserAndAllPets.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/EmbeddedUserAndAllPets.java
new file mode 100644
index 0000000..01326b7
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/EmbeddedUserAndAllPets.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+
+public class EmbeddedUserAndAllPets {
+ @Embedded
+ UserAndAllPets mUserAndAllPets;
+
+ public UserAndAllPets getUserAndAllPets() {
+ return mUserAndAllPets;
+ }
+
+ public void setUserAndAllPets(UserAndAllPets userAndAllPets) {
+ this.mUserAndAllPets = userAndAllPets;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FunnyNamedEntity.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/FunnyNamedEntity.java
new file mode 100644
index 0000000..038a8a4
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/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 androidx.room.integration.testapp.vo;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntAutoIncPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntAutoIncPKeyEntity.java
new file mode 100644
index 0000000..049ee9d
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntAutoIncPKeyEntity.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class IntAutoIncPKeyEntity {
+ @PrimaryKey(autoGenerate = true)
+ public int pKey;
+ public String data;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntegerAutoIncPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntegerAutoIncPKeyEntity.java
new file mode 100644
index 0000000..6ddb217
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntegerAutoIncPKeyEntity.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class IntegerAutoIncPKeyEntity {
+ @PrimaryKey(autoGenerate = true)
+ public Integer pKey;
+ public String data;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntegerPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/IntegerPKeyEntity.java
new file mode 100644
index 0000000..6aa961b
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class IntegerPKeyEntity {
+ @PrimaryKey
+ public Integer pKey;
+ public String data;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/NameAndLastName.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/NameAndLastName.java
new file mode 100644
index 0000000..e2595e3
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/NameAndLastName.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 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 androidx.room.integration.testapp.vo;
+
+
+public class NameAndLastName {
+ private String mName;
+ private String mLastName;
+
+ public NameAndLastName(String name, String lastName) {
+ mName = name;
+ mLastName = lastName;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getLastName() {
+ return mLastName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NameAndLastName that = (NameAndLastName) o;
+
+ if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
+ return mLastName != null ? mLastName.equals(that.mLastName) : that.mLastName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName != null ? mName.hashCode() : 0;
+ result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/ObjectPKeyEntity.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/ObjectPKeyEntity.java
new file mode 100644
index 0000000..0a18a9a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/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 androidx.room.integration.testapp.vo;
+
+import androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@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/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Pet.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Pet.java
new file mode 100644
index 0000000..7eb04a0
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Pet.java
@@ -0,0 +1,99 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+import java.util.Date;
+
+@Entity
+public class Pet {
+ @PrimaryKey
+ private int mPetId;
+ private int mUserId;
+ @ColumnInfo(name = "mPetName")
+ private String mName;
+
+ private Date mAdoptionDate;
+
+ public int getPetId() {
+ return mPetId;
+ }
+
+ public void setPetId(int petId) {
+ mPetId = petId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public void setUserId(int userId) {
+ mUserId = userId;
+ }
+
+ public Date getAdoptionDate() {
+ return mAdoptionDate;
+ }
+
+ public void setAdoptionDate(Date adoptionDate) {
+ mAdoptionDate = adoptionDate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Pet pet = (Pet) o;
+
+ if (mPetId != pet.mPetId) return false;
+ if (mUserId != pet.mUserId) return false;
+ if (mName != null ? !mName.equals(pet.mName) : pet.mName != null) return false;
+ return mAdoptionDate != null ? mAdoptionDate.equals(pet.mAdoptionDate)
+ : pet.mAdoptionDate == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mPetId;
+ result = 31 * result + mUserId;
+ result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + (mAdoptionDate != null ? mAdoptionDate.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Pet{"
+ + "mPetId=" + mPetId
+ + ", mUserId=" + mUserId
+ + ", mName='" + mName + '\''
+ + ", mAdoptionDate=" + mAdoptionDate
+ + '}';
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetAndToys.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetAndToys.java
new file mode 100644
index 0000000..4f1d225
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetAndToys.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class PetAndToys {
+ @Embedded
+ public Pet pet;
+ @Relation(parentColumn = "mPetId", entityColumn = "mPetId")
+ public List<Toy> toys;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetCouple.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetCouple.java
new file mode 100644
index 0000000..df612a7
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetCouple.java
@@ -0,0 +1,61 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.annotation.NonNull;
+import androidx.room.Embedded;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+import androidx.room.RoomWarnings;
+
+@Entity
+@SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED)
+public class PetCouple {
+ @PrimaryKey
+ @NonNull
+ public String id;
+ @Embedded(prefix = "male_")
+ public Pet male;
+ @Embedded(prefix = "female_")
+ private Pet mFemale;
+
+ public Pet getFemale() {
+ return mFemale;
+ }
+
+ public void setFemale(Pet female) {
+ mFemale = female;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PetCouple petCouple = (PetCouple) o;
+
+ if (male != null ? !male.equals(petCouple.male) : petCouple.male != null) return false;
+ return mFemale != null ? mFemale.equals(petCouple.mFemale) : petCouple.mFemale == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = male != null ? male.hashCode() : 0;
+ result = 31 * result + (mFemale != null ? mFemale.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetWithToyIds.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetWithToyIds.java
new file mode 100644
index 0000000..7510444
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetWithToyIds.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Ignore;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class PetWithToyIds {
+ @Embedded
+ public final Pet pet;
+ @Relation(parentColumn = "mPetId", entityColumn = "mPetId", projection = "mId",
+ entity = Toy.class)
+ public List<Integer> toyIds;
+
+ // for the relation
+ public PetWithToyIds(Pet pet) {
+ this.pet = pet;
+ }
+
+ @Ignore
+ public PetWithToyIds(Pet pet, List<Integer> toyIds) {
+ this.pet = pet;
+ this.toyIds = toyIds;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PetWithToyIds that = (PetWithToyIds) o;
+
+ if (pet != null ? !pet.equals(that.pet) : that.pet != null) return false;
+ return toyIds != null ? toyIds.equals(that.toyIds) : that.toyIds == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = pet != null ? pet.hashCode() : 0;
+ result = 31 * result + (toyIds != null ? toyIds.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "PetWithToyIds{"
+ + "pet=" + pet
+ + ", toyIds=" + toyIds
+ + '}';
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetsToys.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetsToys.java
new file mode 100644
index 0000000..4196067
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/PetsToys.java
@@ -0,0 +1,56 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Relation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PetsToys {
+ public int petId;
+ @Relation(parentColumn = "petId", entityColumn = "mPetId")
+ public List<Toy> toys;
+
+ public PetsToys() {
+ toys = new ArrayList<>();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ PetsToys petsToys = (PetsToys) o;
+
+ if (petId != petsToys.petId) {
+ return false;
+ }
+ return toys != null ? toys.equals(petsToys.toys) : petsToys.toys == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = petId;
+ result = 31 * result + (toys != null ? toys.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Product.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Product.java
new file mode 100644
index 0000000..67c903f
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Product.java
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity(tableName = "products")
+public class Product {
+
+ @PrimaryKey(autoGenerate = true)
+ public final int id;
+
+ @NonNull
+ public final String name;
+
+ public Product(int id, @NonNull String name) {
+ this.id = id;
+ this.name = name;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/School.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/School.java
new file mode 100644
index 0000000..610bcd3
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/School.java
@@ -0,0 +1,95 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class School {
+ @PrimaryKey
+ private int mId;
+ private String mName;
+ @Embedded(prefix = "address_")
+ public Address address;
+
+ @Embedded(prefix = "manager_")
+ private User mManager;
+
+ public int getId() {
+ return mId;
+ }
+
+ public void setId(int id) {
+ mId = id;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ public User getManager() {
+ return mManager;
+ }
+
+ public void setManager(User manager) {
+ mManager = manager;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ School school = (School) o;
+
+ if (mId != school.mId) {
+ return false;
+ }
+ if (mName != null ? !mName.equals(school.mName) : school.mName != null) {
+ return false;
+ }
+ if (address != null ? !address.equals(school.address) : school.address != null) {
+ return false;
+ }
+ return mManager != null ? mManager.equals(school.mManager) : school.mManager == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId;
+ result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + (address != null ? address.hashCode() : 0);
+ result = 31 * result + (mManager != null ? mManager.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/SchoolRef.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/SchoolRef.java
new file mode 100644
index 0000000..cbcfd75
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/SchoolRef.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 androidx.room.integration.testapp.vo;
+
+public class SchoolRef extends School {
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Toy.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Toy.java
new file mode 100644
index 0000000..ebc64e6
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/Toy.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+/**
+ * The toys of a pet.
+ */
+@Entity
+public class Toy {
+ @PrimaryKey(autoGenerate = true)
+ private int mId;
+ private String mName;
+ private int mPetId;
+
+ public int getId() {
+ return mId;
+ }
+
+ public void setId(int id) {
+ mId = id;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ public int getPetId() {
+ return mPetId;
+ }
+
+ public void setPetId(int petId) {
+ mPetId = petId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Toy toy = (Toy) o;
+
+ if (mId != toy.mId) return false;
+ if (mPetId != toy.mPetId) return false;
+ return mName != null ? mName.equals(toy.mName) : toy.mName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId;
+ result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + mPetId;
+ return result;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/User.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/User.java
new file mode 100644
index 0000000..af86b7a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/User.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+import androidx.room.TypeConverters;
+import androidx.room.integration.testapp.TestDatabase;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+@TypeConverters({TestDatabase.Converters.class})
+public class User {
+
+ @PrimaryKey
+ private int mId;
+
+ private String mName;
+
+ private String mLastName;
+
+ private int mAge;
+
+ private boolean mAdmin;
+
+ private float mWeight;
+
+ private Date mBirthday;
+
+ @ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE)
+ private String mCustomField;
+
+ // bit flags
+ private Set<Day> mWorkDays = new HashSet<>();
+
+ public int getId() {
+ return mId;
+ }
+
+ public void setId(int id) {
+ this.mId = id;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ this.mName = name;
+ }
+
+ public String getLastName() {
+ return mLastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.mLastName = lastName;
+ }
+
+ public int getAge() {
+ return mAge;
+ }
+
+ public void setAge(int age) {
+ this.mAge = age;
+ }
+
+ public boolean isAdmin() {
+ return mAdmin;
+ }
+
+ public void setAdmin(boolean admin) {
+ mAdmin = admin;
+ }
+
+ public float getWeight() {
+ return mWeight;
+ }
+
+ public void setWeight(float weight) {
+ mWeight = weight;
+ }
+
+ public Date getBirthday() {
+ return mBirthday;
+ }
+
+ public void setBirthday(Date birthday) {
+ mBirthday = birthday;
+ }
+
+ public String getCustomField() {
+ return mCustomField;
+ }
+
+ public void setCustomField(String customField) {
+ mCustomField = customField;
+ }
+
+ public Set<Day> getWorkDays() {
+ return mWorkDays;
+ }
+
+ public void setWorkDays(
+ Set<Day> workDays) {
+ mWorkDays = workDays;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ User user = (User) o;
+
+ if (mId != user.mId) return false;
+ if (mAge != user.mAge) return false;
+ if (mAdmin != user.mAdmin) return false;
+ if (Float.compare(user.mWeight, mWeight) != 0) return false;
+ if (mName != null ? !mName.equals(user.mName) : user.mName != null) return false;
+ if (mLastName != null ? !mLastName.equals(user.mLastName) : user.mLastName != null) {
+ return false;
+ }
+ if (mBirthday != null ? !mBirthday.equals(user.mBirthday) : user.mBirthday != null) {
+ return false;
+ }
+ if (mCustomField != null ? !mCustomField.equals(user.mCustomField)
+ : user.mCustomField != null) {
+ return false;
+ }
+ return mWorkDays != null ? mWorkDays.equals(user.mWorkDays) : user.mWorkDays == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId;
+ result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
+ result = 31 * result + mAge;
+ result = 31 * result + (mAdmin ? 1 : 0);
+ result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
+ result = 31 * result + (mBirthday != null ? mBirthday.hashCode() : 0);
+ result = 31 * result + (mCustomField != null ? mCustomField.hashCode() : 0);
+ result = 31 * result + (mWorkDays != null ? mWorkDays.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "User{"
+ + "mId=" + mId
+ + ", mName='" + mName + '\''
+ + ", mLastName='" + mLastName + '\''
+ + ", mAge=" + mAge
+ + ", mAdmin=" + mAdmin
+ + ", mWeight=" + mWeight
+ + ", mBirthday=" + mBirthday
+ + ", mCustomField='" + mCustomField + '\''
+ + ", mWorkDays=" + mWorkDays
+ + '}';
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPets.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPets.java
new file mode 100644
index 0000000..90332d4
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndAllPets.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class UserAndAllPets {
+ @Embedded
+ public User user;
+ @Relation(parentColumn = "mId", entityColumn = "mUserId")
+ public List<Pet> pets;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPet.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPet.java
new file mode 100644
index 0000000..6b54088
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPet.java
@@ -0,0 +1,41 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+import androidx.room.Embedded;
+
+public class UserAndPet {
+ @Embedded
+ private User mUser;
+ @Embedded
+ private Pet mPet;
+
+ public User getUser() {
+ return mUser;
+ }
+
+ public void setUser(User user) {
+ mUser = user;
+ }
+
+ public Pet getPet() {
+ return mPet;
+ }
+
+ public void setPet(Pet pet) {
+ mPet = pet;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPetAdoptionDates.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPetAdoptionDates.java
new file mode 100644
index 0000000..24d13a6
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPetAdoptionDates.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Ignore;
+import androidx.room.Relation;
+
+import java.util.Date;
+import java.util.List;
+
+public class UserAndPetAdoptionDates {
+ @Embedded
+ public final User user;
+ @Relation(entity = Pet.class,
+ parentColumn = "mId",
+ entityColumn = "mUserId",
+ projection = "mAdoptionDate")
+ public List<Date> petAdoptionDates;
+
+ public UserAndPetAdoptionDates(User user) {
+ this.user = user;
+ }
+
+ @Ignore
+ public UserAndPetAdoptionDates(User user, List<Date> petAdoptionDates) {
+ this.user = user;
+ this.petAdoptionDates = petAdoptionDates;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ UserAndPetAdoptionDates that = (UserAndPetAdoptionDates) o;
+
+ if (user != null ? !user.equals(that.user) : that.user != null) return false;
+ return petAdoptionDates != null ? petAdoptionDates.equals(that.petAdoptionDates)
+ : that.petAdoptionDates == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = user != null ? user.hashCode() : 0;
+ result = 31 * result + (petAdoptionDates != null ? petAdoptionDates.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "UserAndPetAdoptionDates{"
+ + "user=" + user
+ + ", petAdoptionDates=" + petAdoptionDates
+ + '}';
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPetNonNull.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPetNonNull.java
new file mode 100644
index 0000000..ef60a1a
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserAndPetNonNull.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 androidx.room.integration.testapp.vo;
+
+import androidx.annotation.NonNull;
+import androidx.room.Embedded;
+
+public class UserAndPetNonNull {
+ @Embedded
+ private User mUser;
+ @Embedded
+ @NonNull
+ private Pet mPet;
+
+ public User getUser() {
+ return mUser;
+ }
+
+ public void setUser(User user) {
+ mUser = user;
+ }
+
+ public Pet getPet() {
+ return mPet;
+ }
+
+ public void setPet(Pet pet) {
+ mPet = pet;
+ }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserIdAndPetNames.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserIdAndPetNames.java
new file mode 100644
index 0000000..96788c9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserIdAndPetNames.java
@@ -0,0 +1,33 @@
+/*
+ * 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 androidx.room.integration.testapp.vo;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Relation;
+
+import java.util.List;
+
+/**
+ * Same as Pet class but only keeps name and user id
+ */
+public class UserIdAndPetNames {
+ @ColumnInfo(name = "mId")
+ public int userId;
+ @Relation(entity = Pet.class, parentColumn = "mId", entityColumn = "mUserId",
+ projection = "mPetName")
+ public List<String> names;
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserWithPetsAndToys.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserWithPetsAndToys.java
new file mode 100644
index 0000000..ffad720
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/vo/UserWithPetsAndToys.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 androidx.room.integration.testapp.vo;
+
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class UserWithPetsAndToys {
+ @Embedded
+ public User user;
+ @Relation(entity = Pet.class, parentColumn = "mId", entityColumn = "mUserId")
+ public List<PetAndToys> pets;
+}
diff --git a/room/integration-tests/testapp/src/main/AndroidManifest.xml b/room/integration-tests/testapp/src/main/AndroidManifest.xml
index 02c1975..245f9c0 100644
--- a/room/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/room/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -15,12 +15,12 @@
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.room.integration.testapp">
+ package="androidx.room.integration.testapp">
<application
android:allowBackup="true"
android:supportsRtl="true">
<activity
- android:name=".RoomPagedListActivity"
+ android:name="androidx.room.integration.testapp.RoomPagedListActivity"
android:label="Room Live PagedList"
android:theme="@style/Theme.AppCompat">
<intent-filter>
@@ -29,7 +29,7 @@
</intent-filter>
</activity>
<activity
- android:name=".RoomKeyedPagedListActivity"
+ android:name="androidx.room.integration.testapp.RoomKeyedPagedListActivity"
android:label="Keyed Live PagedList"
android:theme="@style/Theme.AppCompat">
<intent-filter>
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
deleted file mode 100644
index d8cd8d4..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/CustomerViewModel.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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;
-
-import android.app.Application;
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.lifecycle.AndroidViewModel;
-import android.arch.lifecycle.LiveData;
-import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListBuilder;
-import android.arch.paging.PagedList;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.integration.testapp.database.Customer;
-import android.arch.persistence.room.integration.testapp.database.LastNameAscCustomerDataSource;
-import android.arch.persistence.room.integration.testapp.database.SampleDatabase;
-import android.support.annotation.WorkerThread;
-
-import java.util.UUID;
-
-/**
- * Sample database-backed view model of Customers
- */
-public class CustomerViewModel extends AndroidViewModel {
- private SampleDatabase mDatabase;
- private LiveData<PagedList<Customer>> mLiveCustomerList;
-
- public CustomerViewModel(Application application) {
- super(application);
- createDb();
- }
-
- private void createDb() {
- mDatabase = Room.databaseBuilder(this.getApplication(),
- SampleDatabase.class, "customerDatabase").build();
-
- ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
- @Override
- public void run() {
- // fill with some simple data
- int customerCount = mDatabase.getCustomerDao().countCustomers();
- if (customerCount == 0) {
- Customer[] initialCustomers = new Customer[10];
- for (int i = 0; i < 10; i++) {
- initialCustomers[i] = createCustomer();
- }
- mDatabase.getCustomerDao().insertAll(initialCustomers);
- }
-
- }
- });
- }
-
- @WorkerThread
- private Customer createCustomer() {
- Customer customer = new Customer();
- customer.setName(UUID.randomUUID().toString());
- customer.setLastName(UUID.randomUUID().toString());
- return customer;
- }
-
- void insertCustomer() {
- ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
- @Override
- public void run() {
- mDatabase.getCustomerDao().insert(createCustomer());
- }
- });
- }
-
- private static <K> LiveData<PagedList<Customer>> getLivePagedList(
- K initialLoadKey, DataSource.Factory<K, Customer> dataSourceFactory) {
- PagedList.Config config = new PagedList.Config.Builder()
- .setPageSize(10)
- .setEnablePlaceholders(false)
- .build();
- return new LivePagedListBuilder<>(dataSourceFactory, config)
- .setInitialLoadKey(initialLoadKey)
- .build();
- }
-
- LiveData<PagedList<Customer>> getLivePagedList(int position) {
- if (mLiveCustomerList == null) {
- mLiveCustomerList =
- getLivePagedList(position, mDatabase.getCustomerDao().loadPagedAgeOrder());
- }
- return mLiveCustomerList;
- }
-
- LiveData<PagedList<Customer>> getLivePagedList(String key) {
- if (mLiveCustomerList == null) {
- mLiveCustomerList =
- getLivePagedList(key, LastNameAscCustomerDataSource.factory(mDatabase));
- }
- return mLiveCustomerList;
- }
-}
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/PagedListCustomerAdapter.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/PagedListCustomerAdapter.java
deleted file mode 100644
index eacea99..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/PagedListCustomerAdapter.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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;
-
-import android.arch.paging.PagedList;
-import android.arch.paging.PagedListAdapter;
-import android.arch.persistence.room.integration.testapp.database.Customer;
-import android.arch.persistence.room.integration.testapp.database.LastNameAscCustomerDataSource;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-/**
- * Sample adapter which uses a AsyncPagedListDiffer.
- */
-class PagedListCustomerAdapter extends PagedListAdapter<Customer, RecyclerView.ViewHolder> {
- private RecyclerView mRecyclerView;
- private boolean mSetObserved;
- private int mScrollToPosition = -1;
- private String mScrollToKey = null;
-
- PagedListCustomerAdapter() {
- super(Customer.DIFF_CALLBACK);
- }
-
- void setScrollToPosition(int position) {
- mScrollToPosition = position;
- }
-
- void setScrollToKey(String key) {
- mScrollToKey = key;
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(
- new TextView(parent.getContext())) {};
- holder.itemView.setMinimumHeight(400);
- return holder;
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
- Customer customer = getItem(position);
-
- if (customer != null) {
- ((TextView) (holder.itemView)).setText(customer.getId() + " " + customer.getLastName());
- } else {
- ((TextView) (holder.itemView)).setText(R.string.loading);
- }
- }
-
- private static int findKeyInPagedList(@NonNull String key, @NonNull PagedList<Customer> list) {
- for (int i = 0; i < list.size(); i++) {
- @Nullable Customer customer = list.get(i);
- if (customer != null
- && LastNameAscCustomerDataSource.getKeyStatic(customer).equals(key)) {
- return i;
- }
- }
- return 0; // couldn't find, fall back to 0 - could alternately search with comparator
- }
-
- @Override
- public void submitList(PagedList<Customer> pagedList) {
- super.submitList(pagedList);
-
- if (pagedList != null) {
- final boolean firstSet = !mSetObserved;
- mSetObserved = true;
-
- if (firstSet
- && mRecyclerView != null
- && (mScrollToPosition >= 0 || mScrollToKey != null)) {
- int localScrollToPosition;
- if (mScrollToKey != null) {
- localScrollToPosition = findKeyInPagedList(mScrollToKey, pagedList);
- mScrollToKey = null;
- } else {
- // if there's 20 items unloaded items (without placeholders holding the spots)
- // at the beginning of list, we subtract 20 from saved position
- localScrollToPosition = mScrollToPosition - pagedList.getPositionOffset();
- }
- mRecyclerView.scrollToPosition(localScrollToPosition);
- }
- }
- }
-
- @Override
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
- mRecyclerView = recyclerView;
- }
-
- @Override
- public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
- mRecyclerView = null;
- }
-}
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomKeyedPagedListActivity.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomKeyedPagedListActivity.java
deleted file mode 100644
index 17ac4ce..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomKeyedPagedListActivity.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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;
-
-/**
- * Sample PagedList activity which uses Room.
- */
-public class RoomKeyedPagedListActivity extends RoomPagedListActivity {
- @Override
- protected boolean useKeyedQuery() {
- return true;
- }
-}
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
deleted file mode 100644
index 66620ef..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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;
-
-import android.arch.lifecycle.LiveData;
-import android.arch.lifecycle.ViewModelProviders;
-import android.arch.paging.PagedList;
-import android.arch.persistence.room.integration.testapp.database.Customer;
-import android.arch.persistence.room.integration.testapp.database.LastNameAscCustomerDataSource;
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.widget.Button;
-
-/**
- * Sample PagedList activity which uses Room.
- */
-public class RoomPagedListActivity extends AppCompatActivity {
-
- private RecyclerView mRecyclerView;
- private PagedListCustomerAdapter mAdapter;
-
- private static final String STRING_KEY = "STRING_KEY";
- private static final String INT_KEY = "INT_KEY";
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_recycler_view);
- final CustomerViewModel viewModel = ViewModelProviders.of(this)
- .get(CustomerViewModel.class);
-
- mRecyclerView = findViewById(R.id.recyclerview);
- mAdapter = new PagedListCustomerAdapter();
- mRecyclerView.setAdapter(mAdapter);
-
- LiveData<PagedList<Customer>> livePagedList;
- if (useKeyedQuery()) {
- String key = null;
- if (savedInstanceState != null) {
- key = savedInstanceState.getString(STRING_KEY);
- mAdapter.setScrollToKey(key);
- }
- livePagedList = viewModel.getLivePagedList(key);
- } else {
- int position = 0;
- if (savedInstanceState != null) {
- position = savedInstanceState.getInt(INT_KEY);
- mAdapter.setScrollToPosition(position);
- }
- livePagedList = viewModel.getLivePagedList(position);
- }
- livePagedList.observe(this, items -> mAdapter.submitList(items));
- final Button button = findViewById(R.id.addButton);
- button.setOnClickListener(v -> viewModel.insertCustomer());
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- PagedList<Customer> list = mAdapter.getCurrentList();
- if (list == null) {
- // Can't find anything to restore
- return;
- }
-
- LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
- final int targetPosition = layoutManager.findFirstVisibleItemPosition();
-
- if (useKeyedQuery()) {
- Customer customer = list.get(targetPosition);
- if (customer != null) {
- String key = LastNameAscCustomerDataSource.getKeyStatic(customer);
- outState.putString(STRING_KEY, key);
- }
- } else {
- // NOTE: in the general case, we can't just rely on RecyclerView/LinearLayoutManager to
- // preserve position, because of position offset which is present when using an
- // uncounted, non-keyed source).
- int absolutePosition = targetPosition + list.getPositionOffset();
- outState.putInt(INT_KEY, absolutePosition);
- }
- }
-
- protected boolean useKeyedQuery() {
- return false;
- }
-}
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/Customer.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/Customer.java
deleted file mode 100644
index 54fa531..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/Customer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2016 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.database;
-
-import android.arch.persistence.room.Entity;
-import android.arch.persistence.room.PrimaryKey;
-import android.support.annotation.NonNull;
-import android.support.v7.util.DiffUtil;
-
-/**
- * Sample entity
- */
-@Entity
-public class Customer {
-
- @PrimaryKey(autoGenerate = true)
- private int mId;
-
- private String mName;
-
- private String mLastName;
-
- public int getId() {
- return mId;
- }
-
- public void setId(int id) {
- this.mId = id;
- }
-
- public String getName() {
- return mName;
- }
-
- public void setName(String name) {
- this.mName = name;
- }
-
- public String getLastName() {
- return mLastName;
- }
-
- public void setLastName(String lastName) {
- this.mLastName = lastName;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- Customer customer = (Customer) o;
-
- if (mId != customer.mId) {
- return false;
- }
- if (mName != null ? !mName.equals(customer.mName) : customer.mName != null) {
- return false;
- }
- return mLastName != null ? mLastName.equals(customer.mLastName)
- : customer.mLastName == null;
- }
-
- @Override
- public int hashCode() {
- int result = mId;
- result = 31 * result + (mName != null ? mName.hashCode() : 0);
- result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return "Customer{"
- + "mId=" + mId
- + ", mName='" + mName + '\''
- + ", mLastName='" + mLastName + '\''
- + '}';
- }
-
- public static final DiffUtil.ItemCallback<Customer> DIFF_CALLBACK =
- new DiffUtil.ItemCallback<Customer>() {
- @Override
- public boolean areContentsTheSame(@NonNull Customer oldItem, @NonNull Customer newItem) {
- return oldItem.equals(newItem);
- }
-
- @Override
- public boolean areItemsTheSame(@NonNull Customer oldItem, @NonNull Customer newItem) {
- return oldItem.getId() == newItem.getId();
- }
- };
-}
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
deleted file mode 100644
index db45dc4..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/CustomerDao.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2016 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.database;
-
-import android.arch.paging.DataSource;
-import android.arch.persistence.room.Dao;
-import android.arch.persistence.room.Insert;
-import android.arch.persistence.room.Query;
-
-import java.util.List;
-
-/**
- * Simple Customer DAO for Room Customer list sample.
- */
-@Dao
-public interface CustomerDao {
-
- /**
- * Insert a customer
- * @param customer Customer.
- */
- @Insert
- void insert(Customer customer);
-
- /**
- * Insert multiple customers.
- * @param customers Customers.
- */
- @Insert
- void insertAll(Customer[] customers);
-
- /**
- * @return DataSource.Factory of customers, ordered by last name. Use
- * {@link android.arch.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
- */
- @Query("SELECT * FROM customer ORDER BY mLastName ASC")
- DataSource.Factory<Integer, Customer> loadPagedAgeOrder();
-
- /**
- * @return number of customers
- */
- @Query("SELECT COUNT(*) FROM customer")
- int countCustomers();
-
- // Keyed
-
- @Query("SELECT * from customer ORDER BY mLastName DESC LIMIT :limit")
- List<Customer> customerNameInitial(int limit);
-
- @Query("SELECT * from customer WHERE mLastName < :key ORDER BY mLastName DESC LIMIT :limit")
- List<Customer> customerNameLoadAfter(String key, int limit);
-
- @Query("SELECT COUNT(*) from customer WHERE mLastName < :key ORDER BY mLastName DESC")
- int customerNameCountAfter(String key);
-
- @Query("SELECT * from customer WHERE mLastName > :key ORDER BY mLastName ASC LIMIT :limit")
- List<Customer> customerNameLoadBefore(String key, int limit);
-
- @Query("SELECT COUNT(*) from customer WHERE mLastName > :key ORDER BY mLastName ASC")
- int customerNameCountBefore(String key);
-}
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
deleted file mode 100644
index 6278bc2..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.database;
-
-import android.arch.paging.DataSource;
-import android.arch.paging.ItemKeyedDataSource;
-import android.arch.persistence.room.InvalidationTracker;
-import android.support.annotation.NonNull;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Sample Room keyed data source.
- */
-public class LastNameAscCustomerDataSource extends ItemKeyedDataSource<String, Customer> {
- private final CustomerDao mCustomerDao;
- @SuppressWarnings("FieldCanBeLocal")
- private final InvalidationTracker.Observer mObserver;
- private SampleDatabase mDb;
-
- public static Factory<String, Customer> factory(final SampleDatabase db) {
- return new Factory<String, Customer>() {
- @Override
- public DataSource<String, Customer> create() {
- return new LastNameAscCustomerDataSource(db);
- }
- };
- }
-
- /**
- * Create a DataSource from the customer table of the given database
- */
- private LastNameAscCustomerDataSource(SampleDatabase db) {
- mDb = db;
- mCustomerDao = db.getCustomerDao();
- mObserver = new InvalidationTracker.Observer("customer") {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- invalidate();
- }
- };
- db.getInvalidationTracker().addWeakObserver(mObserver);
- }
-
- @Override
- public boolean isInvalid() {
- mDb.getInvalidationTracker().refreshVersionsSync();
- return super.isInvalid();
- }
-
- @NonNull
- public static String getKeyStatic(@NonNull Customer customer) {
- return customer.getLastName();
- }
-
- @NonNull
- @Override
- public String getKey(@NonNull Customer customer) {
- return getKeyStatic(customer);
- }
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams<String> params,
- @NonNull LoadInitialCallback<Customer> callback) {
- String customerName = params.requestedInitialKey;
- List<Customer> list;
- if (customerName != null) {
- // initial keyed load - load before 'customerName',
- // and load after last item in before list
- int pageSize = params.requestedLoadSize / 2;
- String key = customerName;
- list = mCustomerDao.customerNameLoadBefore(key, pageSize);
- Collections.reverse(list);
- if (!list.isEmpty()) {
- key = getKey(list.get(list.size() - 1));
- }
- list.addAll(mCustomerDao.customerNameLoadAfter(key, pageSize));
- } else {
- list = mCustomerDao.customerNameInitial(params.requestedLoadSize);
- }
-
- if (params.placeholdersEnabled && !list.isEmpty()) {
- String firstKey = getKey(list.get(0));
- String lastKey = getKey(list.get(list.size() - 1));
-
- // only bother counting if placeholders are desired
- final int position = mCustomerDao.customerNameCountBefore(firstKey);
- final int count = position + list.size() + mCustomerDao.customerNameCountAfter(lastKey);
- callback.onResult(list, position, count);
- } else {
- callback.onResult(list);
- }
- }
-
- @Override
- public void loadAfter(@NonNull LoadParams<String> params,
- @NonNull LoadCallback<Customer> callback) {
- callback.onResult(mCustomerDao.customerNameLoadAfter(params.key, params.requestedLoadSize));
- }
-
- @Override
- public void loadBefore(@NonNull LoadParams<String> params,
- @NonNull LoadCallback<Customer> callback) {
- List<Customer> list = mCustomerDao.customerNameLoadBefore(
- params.key, params.requestedLoadSize);
- Collections.reverse(list);
- callback.onResult(list);
- }
-}
-
diff --git a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java b/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java
deleted file mode 100644
index 9020eb1..0000000
--- a/room/integration-tests/testapp/src/main/java/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.database;
-
-import android.arch.persistence.room.Database;
-import android.arch.persistence.room.RoomDatabase;
-
-/**
- * Sample database of customers.
- */
-@Database(entities = {Customer.class},
- version = 1, exportSchema = false)
-public abstract class SampleDatabase extends RoomDatabase {
- /**
- * @return customer dao.
- */
- public abstract CustomerDao getCustomerDao();
-}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/CustomerViewModel.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/CustomerViewModel.java
new file mode 100644
index 0000000..eca2552
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/CustomerViewModel.java
@@ -0,0 +1,111 @@
+/*
+ * 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 androidx.room.integration.testapp;
+
+import android.app.Application;
+
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.paging.DataSource;
+import androidx.paging.LivePagedListBuilder;
+import androidx.paging.PagedList;
+import androidx.room.Room;
+import androidx.room.integration.testapp.database.Customer;
+import androidx.room.integration.testapp.database.LastNameAscCustomerDataSource;
+import androidx.room.integration.testapp.database.SampleDatabase;
+
+import java.util.UUID;
+
+/**
+ * Sample database-backed view model of Customers
+ */
+public class CustomerViewModel extends AndroidViewModel {
+ private SampleDatabase mDatabase;
+ private LiveData<PagedList<Customer>> mLiveCustomerList;
+
+ public CustomerViewModel(Application application) {
+ super(application);
+ createDb();
+ }
+
+ private void createDb() {
+ mDatabase = Room.databaseBuilder(this.getApplication(),
+ SampleDatabase.class, "customerDatabase").build();
+
+ ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
+ @Override
+ public void run() {
+ // fill with some simple data
+ int customerCount = mDatabase.getCustomerDao().countCustomers();
+ if (customerCount == 0) {
+ Customer[] initialCustomers = new Customer[10];
+ for (int i = 0; i < 10; i++) {
+ initialCustomers[i] = createCustomer();
+ }
+ mDatabase.getCustomerDao().insertAll(initialCustomers);
+ }
+
+ }
+ });
+ }
+
+ @WorkerThread
+ private Customer createCustomer() {
+ Customer customer = new Customer();
+ customer.setName(UUID.randomUUID().toString());
+ customer.setLastName(UUID.randomUUID().toString());
+ return customer;
+ }
+
+ void insertCustomer() {
+ ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
+ @Override
+ public void run() {
+ mDatabase.getCustomerDao().insert(createCustomer());
+ }
+ });
+ }
+
+ private static <K> LiveData<PagedList<Customer>> getLivePagedList(
+ K initialLoadKey, DataSource.Factory<K, Customer> dataSourceFactory) {
+ PagedList.Config config = new PagedList.Config.Builder()
+ .setPageSize(10)
+ .setEnablePlaceholders(false)
+ .build();
+ return new LivePagedListBuilder<>(dataSourceFactory, config)
+ .setInitialLoadKey(initialLoadKey)
+ .build();
+ }
+
+ LiveData<PagedList<Customer>> getLivePagedList(int position) {
+ if (mLiveCustomerList == null) {
+ mLiveCustomerList =
+ getLivePagedList(position, mDatabase.getCustomerDao().loadPagedAgeOrder());
+ }
+ return mLiveCustomerList;
+ }
+
+ LiveData<PagedList<Customer>> getLivePagedList(String key) {
+ if (mLiveCustomerList == null) {
+ mLiveCustomerList =
+ getLivePagedList(key, LastNameAscCustomerDataSource.factory(mDatabase));
+ }
+ return mLiveCustomerList;
+ }
+}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/PagedListCustomerAdapter.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/PagedListCustomerAdapter.java
new file mode 100644
index 0000000..8c0a305
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/PagedListCustomerAdapter.java
@@ -0,0 +1,115 @@
+/*
+ * 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 androidx.room.integration.testapp;
+
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.paging.PagedList;
+import androidx.paging.PagedListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.room.integration.testapp.database.Customer;
+import androidx.room.integration.testapp.database.LastNameAscCustomerDataSource;
+
+/**
+ * Sample adapter which uses a AsyncPagedListDiffer.
+ */
+class PagedListCustomerAdapter extends PagedListAdapter<Customer, RecyclerView.ViewHolder> {
+ private RecyclerView mRecyclerView;
+ private boolean mSetObserved;
+ private int mScrollToPosition = -1;
+ private String mScrollToKey = null;
+
+ PagedListCustomerAdapter() {
+ super(Customer.DIFF_CALLBACK);
+ }
+
+ void setScrollToPosition(int position) {
+ mScrollToPosition = position;
+ }
+
+ void setScrollToKey(String key) {
+ mScrollToKey = key;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(
+ new TextView(parent.getContext())) {};
+ holder.itemView.setMinimumHeight(400);
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ Customer customer = getItem(position);
+
+ if (customer != null) {
+ ((TextView) (holder.itemView)).setText(customer.getId() + " " + customer.getLastName());
+ } else {
+ ((TextView) (holder.itemView)).setText(R.string.loading);
+ }
+ }
+
+ private static int findKeyInPagedList(@NonNull String key, @NonNull PagedList<Customer> list) {
+ for (int i = 0; i < list.size(); i++) {
+ @Nullable Customer customer = list.get(i);
+ if (customer != null
+ && LastNameAscCustomerDataSource.getKeyStatic(customer).equals(key)) {
+ return i;
+ }
+ }
+ return 0; // couldn't find, fall back to 0 - could alternately search with comparator
+ }
+
+ @Override
+ public void submitList(PagedList<Customer> pagedList) {
+ super.submitList(pagedList);
+
+ if (pagedList != null) {
+ final boolean firstSet = !mSetObserved;
+ mSetObserved = true;
+
+ if (firstSet
+ && mRecyclerView != null
+ && (mScrollToPosition >= 0 || mScrollToKey != null)) {
+ int localScrollToPosition;
+ if (mScrollToKey != null) {
+ localScrollToPosition = findKeyInPagedList(mScrollToKey, pagedList);
+ mScrollToKey = null;
+ } else {
+ // if there's 20 items unloaded items (without placeholders holding the spots)
+ // at the beginning of list, we subtract 20 from saved position
+ localScrollToPosition = mScrollToPosition - pagedList.getPositionOffset();
+ }
+ mRecyclerView.scrollToPosition(localScrollToPosition);
+ }
+ }
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ mRecyclerView = null;
+ }
+}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/RoomKeyedPagedListActivity.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/RoomKeyedPagedListActivity.java
new file mode 100644
index 0000000..4f146c6
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/RoomKeyedPagedListActivity.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 androidx.room.integration.testapp;
+
+/**
+ * Sample PagedList activity which uses Room.
+ */
+public class RoomKeyedPagedListActivity extends RoomPagedListActivity {
+ @Override
+ protected boolean useKeyedQuery() {
+ return true;
+ }
+}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/RoomPagedListActivity.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/RoomPagedListActivity.java
new file mode 100644
index 0000000..b82d688
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/RoomPagedListActivity.java
@@ -0,0 +1,104 @@
+/*
+ * 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 androidx.room.integration.testapp;
+
+import android.os.Bundle;
+import android.widget.Button;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.paging.PagedList;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.room.integration.testapp.database.Customer;
+import androidx.room.integration.testapp.database.LastNameAscCustomerDataSource;
+
+/**
+ * Sample PagedList activity which uses Room.
+ */
+public class RoomPagedListActivity extends AppCompatActivity {
+
+ private RecyclerView mRecyclerView;
+ private PagedListCustomerAdapter mAdapter;
+
+ private static final String STRING_KEY = "STRING_KEY";
+ private static final String INT_KEY = "INT_KEY";
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_recycler_view);
+ final CustomerViewModel viewModel = ViewModelProviders.of(this)
+ .get(CustomerViewModel.class);
+
+ mRecyclerView = findViewById(R.id.recyclerview);
+ mAdapter = new PagedListCustomerAdapter();
+ mRecyclerView.setAdapter(mAdapter);
+
+ LiveData<PagedList<Customer>> livePagedList;
+ if (useKeyedQuery()) {
+ String key = null;
+ if (savedInstanceState != null) {
+ key = savedInstanceState.getString(STRING_KEY);
+ mAdapter.setScrollToKey(key);
+ }
+ livePagedList = viewModel.getLivePagedList(key);
+ } else {
+ int position = 0;
+ if (savedInstanceState != null) {
+ position = savedInstanceState.getInt(INT_KEY);
+ mAdapter.setScrollToPosition(position);
+ }
+ livePagedList = viewModel.getLivePagedList(position);
+ }
+ livePagedList.observe(this, items -> mAdapter.submitList(items));
+ final Button button = findViewById(R.id.addButton);
+ button.setOnClickListener(v -> viewModel.insertCustomer());
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ PagedList<Customer> list = mAdapter.getCurrentList();
+ if (list == null) {
+ // Can't find anything to restore
+ return;
+ }
+
+ LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
+ final int targetPosition = layoutManager.findFirstVisibleItemPosition();
+
+ if (useKeyedQuery()) {
+ Customer customer = list.get(targetPosition);
+ if (customer != null) {
+ String key = LastNameAscCustomerDataSource.getKeyStatic(customer);
+ outState.putString(STRING_KEY, key);
+ }
+ } else {
+ // NOTE: in the general case, we can't just rely on RecyclerView/LinearLayoutManager to
+ // preserve position, because of position offset which is present when using an
+ // uncounted, non-keyed source).
+ int absolutePosition = targetPosition + list.getPositionOffset();
+ outState.putInt(INT_KEY, absolutePosition);
+ }
+ }
+
+ protected boolean useKeyedQuery() {
+ return false;
+ }
+}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/Customer.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/Customer.java
new file mode 100644
index 0000000..1541114
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/Customer.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp.database;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+/**
+ * Sample entity
+ */
+@Entity
+public class Customer {
+
+ @PrimaryKey(autoGenerate = true)
+ private int mId;
+
+ private String mName;
+
+ private String mLastName;
+
+ public int getId() {
+ return mId;
+ }
+
+ public void setId(int id) {
+ this.mId = id;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ this.mName = name;
+ }
+
+ public String getLastName() {
+ return mLastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.mLastName = lastName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Customer customer = (Customer) o;
+
+ if (mId != customer.mId) {
+ return false;
+ }
+ if (mName != null ? !mName.equals(customer.mName) : customer.mName != null) {
+ return false;
+ }
+ return mLastName != null ? mLastName.equals(customer.mLastName)
+ : customer.mLastName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId;
+ result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Customer{"
+ + "mId=" + mId
+ + ", mName='" + mName + '\''
+ + ", mLastName='" + mLastName + '\''
+ + '}';
+ }
+
+ public static final DiffUtil.ItemCallback<Customer> DIFF_CALLBACK =
+ new DiffUtil.ItemCallback<Customer>() {
+ @Override
+ public boolean areContentsTheSame(@NonNull Customer oldItem, @NonNull Customer newItem) {
+ return oldItem.equals(newItem);
+ }
+
+ @Override
+ public boolean areItemsTheSame(@NonNull Customer oldItem, @NonNull Customer newItem) {
+ return oldItem.getId() == newItem.getId();
+ }
+ };
+}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/CustomerDao.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/CustomerDao.java
new file mode 100644
index 0000000..311a593
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/CustomerDao.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.integration.testapp.database;
+
+import androidx.paging.DataSource;
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.Query;
+
+import java.util.List;
+
+/**
+ * Simple Customer DAO for Room Customer list sample.
+ */
+@Dao
+public interface CustomerDao {
+
+ /**
+ * Insert a customer
+ * @param customer Customer.
+ */
+ @Insert
+ void insert(Customer customer);
+
+ /**
+ * Insert multiple customers.
+ * @param customers Customers.
+ */
+ @Insert
+ void insertAll(Customer[] customers);
+
+ /**
+ * @return DataSource.Factory of customers, ordered by last name. Use
+ * {@link androidx.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
+ */
+ @Query("SELECT * FROM customer ORDER BY mLastName ASC")
+ DataSource.Factory<Integer, Customer> loadPagedAgeOrder();
+
+ /**
+ * @return number of customers
+ */
+ @Query("SELECT COUNT(*) FROM customer")
+ int countCustomers();
+
+ // Keyed
+
+ @Query("SELECT * from customer ORDER BY mLastName DESC LIMIT :limit")
+ List<Customer> customerNameInitial(int limit);
+
+ @Query("SELECT * from customer WHERE mLastName < :key ORDER BY mLastName DESC LIMIT :limit")
+ List<Customer> customerNameLoadAfter(String key, int limit);
+
+ @Query("SELECT COUNT(*) from customer WHERE mLastName < :key ORDER BY mLastName DESC")
+ int customerNameCountAfter(String key);
+
+ @Query("SELECT * from customer WHERE mLastName > :key ORDER BY mLastName ASC LIMIT :limit")
+ List<Customer> customerNameLoadBefore(String key, int limit);
+
+ @Query("SELECT COUNT(*) from customer WHERE mLastName > :key ORDER BY mLastName ASC")
+ int customerNameCountBefore(String key);
+}
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/LastNameAscCustomerDataSource.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/LastNameAscCustomerDataSource.java
new file mode 100644
index 0000000..84cc8a2
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/LastNameAscCustomerDataSource.java
@@ -0,0 +1,125 @@
+/*
+ * 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 androidx.room.integration.testapp.database;
+
+import androidx.annotation.NonNull;
+import androidx.paging.DataSource;
+import androidx.paging.ItemKeyedDataSource;
+import androidx.room.InvalidationTracker;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Sample Room keyed data source.
+ */
+public class LastNameAscCustomerDataSource extends ItemKeyedDataSource<String, Customer> {
+ private final CustomerDao mCustomerDao;
+ @SuppressWarnings("FieldCanBeLocal")
+ private final InvalidationTracker.Observer mObserver;
+ private SampleDatabase mDb;
+
+ public static Factory<String, Customer> factory(final SampleDatabase db) {
+ return new Factory<String, Customer>() {
+ @Override
+ public DataSource<String, Customer> create() {
+ return new LastNameAscCustomerDataSource(db);
+ }
+ };
+ }
+
+ /**
+ * Create a DataSource from the customer table of the given database
+ */
+ private LastNameAscCustomerDataSource(SampleDatabase db) {
+ mDb = db;
+ mCustomerDao = db.getCustomerDao();
+ mObserver = new InvalidationTracker.Observer("customer") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ invalidate();
+ }
+ };
+ db.getInvalidationTracker().addWeakObserver(mObserver);
+ }
+
+ @Override
+ public boolean isInvalid() {
+ mDb.getInvalidationTracker().refreshVersionsSync();
+ return super.isInvalid();
+ }
+
+ @NonNull
+ public static String getKeyStatic(@NonNull Customer customer) {
+ return customer.getLastName();
+ }
+
+ @NonNull
+ @Override
+ public String getKey(@NonNull Customer customer) {
+ return getKeyStatic(customer);
+ }
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams<String> params,
+ @NonNull LoadInitialCallback<Customer> callback) {
+ String customerName = params.requestedInitialKey;
+ List<Customer> list;
+ if (customerName != null) {
+ // initial keyed load - load before 'customerName',
+ // and load after last item in before list
+ int pageSize = params.requestedLoadSize / 2;
+ String key = customerName;
+ list = mCustomerDao.customerNameLoadBefore(key, pageSize);
+ Collections.reverse(list);
+ if (!list.isEmpty()) {
+ key = getKey(list.get(list.size() - 1));
+ }
+ list.addAll(mCustomerDao.customerNameLoadAfter(key, pageSize));
+ } else {
+ list = mCustomerDao.customerNameInitial(params.requestedLoadSize);
+ }
+
+ if (params.placeholdersEnabled && !list.isEmpty()) {
+ String firstKey = getKey(list.get(0));
+ String lastKey = getKey(list.get(list.size() - 1));
+
+ // only bother counting if placeholders are desired
+ final int position = mCustomerDao.customerNameCountBefore(firstKey);
+ final int count = position + list.size() + mCustomerDao.customerNameCountAfter(lastKey);
+ callback.onResult(list, position, count);
+ } else {
+ callback.onResult(list);
+ }
+ }
+
+ @Override
+ public void loadAfter(@NonNull LoadParams<String> params,
+ @NonNull LoadCallback<Customer> callback) {
+ callback.onResult(mCustomerDao.customerNameLoadAfter(params.key, params.requestedLoadSize));
+ }
+
+ @Override
+ public void loadBefore(@NonNull LoadParams<String> params,
+ @NonNull LoadCallback<Customer> callback) {
+ List<Customer> list = mCustomerDao.customerNameLoadBefore(
+ params.key, params.requestedLoadSize);
+ Collections.reverse(list);
+ callback.onResult(list);
+ }
+}
+
diff --git a/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/SampleDatabase.java b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/SampleDatabase.java
new file mode 100644
index 0000000..e6c103d
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/java/androidx/room/integration/testapp/database/SampleDatabase.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 androidx.room.integration.testapp.database;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+
+/**
+ * Sample database of customers.
+ */
+@Database(entities = {Customer.class},
+ version = 1, exportSchema = false)
+public abstract class SampleDatabase extends RoomDatabase {
+ /**
+ * @return customer dao.
+ */
+ public abstract CustomerDao getCustomerDao();
+}
diff --git a/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml b/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml
index 34e4491..8934cbb 100644
--- a/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml
+++ b/room/integration-tests/testapp/src/main/res/layout/activity_recycler_view.xml
@@ -21,7 +21,7 @@
android:id="@+id/activity_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context="android.arch.persistence.room.integration.testapp.RoomPagedListActivity">
+ tools:context="androidx.room.integration.testapp.RoomPagedListActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
diff --git a/room/migration/api/1.0.0.txt b/room/migration/api_legacy/1.0.0.txt
similarity index 100%
rename from room/migration/api/1.0.0.txt
rename to room/migration/api_legacy/1.0.0.txt
diff --git a/room/migration/build.gradle b/room/migration/build.gradle
index 2b683e8..715fb62 100644
--- a/room/migration/build.gradle
+++ b/room/migration/build.gradle
@@ -27,7 +27,7 @@
test.java.srcDirs += 'src/tests/kotlin'
}
dependencies {
- compile(project(":room:common"))
+ compile(project(":room:room-common"))
compile(KOTLIN_STDLIB)
compile(GSON)
testCompile(JUNIT)
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/BundleUtil.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/BundleUtil.java
deleted file mode 100644
index 4356e69..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/BundleUtil.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-/**
- * Utility functions for bundling.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class BundleUtil {
- /**
- * Placeholder for table names in queries.
- */
- public static final String TABLE_NAME_PLACEHOLDER = "${TABLE_NAME}";
-
- static String replaceTableName(String contents, String tableName) {
- return contents.replace(TABLE_NAME_PLACEHOLDER, tableName);
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
deleted file mode 100644
index f131838..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Data class that holds the schema information for a
- * {@link android.arch.persistence.room.Database Database}.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class DatabaseBundle implements SchemaEquality<DatabaseBundle> {
- @SerializedName("version")
- private int mVersion;
- @SerializedName("identityHash")
- private String mIdentityHash;
- @SerializedName("entities")
- private List<EntityBundle> mEntities;
- // then entity where we keep room information
- @SerializedName("setupQueries")
- private List<String> mSetupQueries;
- private transient Map<String, EntityBundle> mEntitiesByTableName;
-
- /**
- * Creates a new database
- * @param version Version
- * @param identityHash Identity hash
- * @param entities List of entities
- */
- public DatabaseBundle(int version, String identityHash, List<EntityBundle> entities,
- List<String> setupQueries) {
- mVersion = version;
- mIdentityHash = identityHash;
- mEntities = entities;
- mSetupQueries = setupQueries;
- }
-
- /**
- * @return The identity has of the Database.
- */
- public String getIdentityHash() {
- return mIdentityHash;
- }
-
- /**
- * @return The database version.
- */
- public int getVersion() {
- return mVersion;
- }
-
- /**
- * @return List of entities.
- */
- public List<EntityBundle> getEntities() {
- return mEntities;
- }
-
- /**
- * @return Map of entities, keyed by table name.
- */
- @SuppressWarnings("unused")
- public Map<String, EntityBundle> getEntitiesByTableName() {
- if (mEntitiesByTableName == null) {
- mEntitiesByTableName = new HashMap<>();
- for (EntityBundle bundle : mEntities) {
- mEntitiesByTableName.put(bundle.getTableName(), bundle);
- }
- }
- return mEntitiesByTableName;
- }
-
- /**
- * @return List of SQL queries to build this database from scratch.
- */
- public List<String> buildCreateQueries() {
- List<String> result = new ArrayList<>();
- for (EntityBundle entityBundle : mEntities) {
- result.addAll(entityBundle.buildCreateQueries());
- }
- result.addAll(mSetupQueries);
- return result;
- }
-
- @Override
- public boolean isSchemaEqual(DatabaseBundle other) {
- return SchemaEqualityUtil.checkSchemaEquality(getEntitiesByTableName(),
- other.getEntitiesByTableName());
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
deleted file mode 100644
index d78ac35..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/EntityBundle.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import static android.arch.persistence.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.annotations.SerializedName;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Data class that holds the schema information about an
- * {@link android.arch.persistence.room.Entity Entity}.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class EntityBundle implements SchemaEquality<EntityBundle> {
-
- static final String NEW_TABLE_PREFIX = "_new_";
-
- @SerializedName("tableName")
- private String mTableName;
- @SerializedName("createSql")
- private String mCreateSql;
- @SerializedName("fields")
- private List<FieldBundle> mFields;
- @SerializedName("primaryKey")
- private PrimaryKeyBundle mPrimaryKey;
- @SerializedName("indices")
- private List<IndexBundle> mIndices;
- @SerializedName("foreignKeys")
- private List<ForeignKeyBundle> mForeignKeys;
-
- private transient String mNewTableName;
- private transient Map<String, FieldBundle> mFieldsByColumnName;
-
- /**
- * Creates a new bundle.
- *
- * @param tableName The table name.
- * @param createSql Create query with the table name placeholder.
- * @param fields The list of fields.
- * @param primaryKey The primary key.
- * @param indices The list of indices
- * @param foreignKeys The list of foreign keys
- */
- public EntityBundle(String tableName, String createSql,
- List<FieldBundle> fields,
- PrimaryKeyBundle primaryKey,
- List<IndexBundle> indices,
- List<ForeignKeyBundle> foreignKeys) {
- mTableName = tableName;
- mCreateSql = createSql;
- mFields = fields;
- mPrimaryKey = primaryKey;
- mIndices = indices;
- mForeignKeys = foreignKeys;
- }
-
- /**
- * @return The table name if it is created during a table schema modification.
- */
- public String getNewTableName() {
- if (mNewTableName == null) {
- mNewTableName = NEW_TABLE_PREFIX + mTableName;
- }
- return mNewTableName;
- }
-
- /**
- * @return Map of fields keyed by their column names.
- */
- public Map<String, FieldBundle> getFieldsByColumnName() {
- if (mFieldsByColumnName == null) {
- mFieldsByColumnName = new HashMap<>();
- for (FieldBundle bundle : mFields) {
- mFieldsByColumnName.put(bundle.getColumnName(), bundle);
- }
- }
- return mFieldsByColumnName;
- }
-
- /**
- * @return The table name.
- */
- public String getTableName() {
- return mTableName;
- }
-
- /**
- * @return The create query with table name placeholder.
- */
- public String getCreateSql() {
- return mCreateSql;
- }
-
- /**
- * @return List of fields.
- */
- public List<FieldBundle> getFields() {
- return mFields;
- }
-
- /**
- * @return The primary key description.
- */
- public PrimaryKeyBundle getPrimaryKey() {
- return mPrimaryKey;
- }
-
- /**
- * @return List of indices.
- */
- public List<IndexBundle> getIndices() {
- return mIndices;
- }
-
- /**
- * @return List of foreign keys.
- */
- public List<ForeignKeyBundle> getForeignKeys() {
- return mForeignKeys;
- }
-
- /**
- * @return Create table SQL query that uses the actual table name.
- */
- public String createTable() {
- return BundleUtil.replaceTableName(mCreateSql, getTableName());
- }
-
- /**
- * @return Create table SQL query that uses the table name with "new" prefix.
- */
- public String createNewTable() {
- return BundleUtil.replaceTableName(mCreateSql, getNewTableName());
- }
-
- /**
- * @return Renames the table with {@link #getNewTableName()} to {@link #getTableName()}.
- */
- @NotNull
- public String renameToOriginal() {
- return "ALTER TABLE " + getNewTableName() + " RENAME TO " + getTableName();
- }
-
- /**
- * @return Creates the list of SQL queries that are necessary to create this entity.
- */
- public Collection<String> buildCreateQueries() {
- List<String> result = new ArrayList<>();
- result.add(createTable());
- for (IndexBundle indexBundle : mIndices) {
- result.add(indexBundle.create(getTableName()));
- }
- return result;
- }
-
- @Override
- public boolean isSchemaEqual(EntityBundle other) {
- if (!mTableName.equals(other.mTableName)) {
- return false;
- }
- return checkSchemaEquality(getFieldsByColumnName(), other.getFieldsByColumnName())
- && checkSchemaEquality(mPrimaryKey, other.mPrimaryKey)
- && checkSchemaEquality(mIndices, other.mIndices)
- && checkSchemaEquality(mForeignKeys, other.mForeignKeys);
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
deleted file mode 100644
index 5f74087..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/FieldBundle.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Data class that holds the schema information for an
- * {@link android.arch.persistence.room.Entity Entity} field.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FieldBundle implements SchemaEquality<FieldBundle> {
- @SerializedName("fieldPath")
- private String mFieldPath;
- @SerializedName("columnName")
- private String mColumnName;
- @SerializedName("affinity")
- private String mAffinity;
- @SerializedName("notNull")
- private boolean mNonNull;
-
- public FieldBundle(String fieldPath, String columnName, String affinity, boolean nonNull) {
- mFieldPath = fieldPath;
- mColumnName = columnName;
- mAffinity = affinity;
- mNonNull = nonNull;
- }
-
- public String getFieldPath() {
- return mFieldPath;
- }
-
- public String getColumnName() {
- return mColumnName;
- }
-
- public String getAffinity() {
- return mAffinity;
- }
-
- public boolean isNonNull() {
- return mNonNull;
- }
-
- @Override
- public boolean isSchemaEqual(FieldBundle other) {
- if (mNonNull != other.mNonNull) return false;
- if (mColumnName != null ? !mColumnName.equals(other.mColumnName)
- : other.mColumnName != null) {
- return false;
- }
- return mAffinity != null ? mAffinity.equals(other.mAffinity) : other.mAffinity == null;
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
deleted file mode 100644
index 367dd74..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.util.List;
-
-/**
- * Holds the information about a foreign key reference.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ForeignKeyBundle implements SchemaEquality<ForeignKeyBundle> {
- @SerializedName("table")
- private String mTable;
- @SerializedName("onDelete")
- private String mOnDelete;
- @SerializedName("onUpdate")
- private String mOnUpdate;
- @SerializedName("columns")
- private List<String> mColumns;
- @SerializedName("referencedColumns")
- private List<String> mReferencedColumns;
-
- /**
- * Creates a foreign key bundle with the given parameters.
- *
- * @param table The target table
- * @param onDelete OnDelete action
- * @param onUpdate OnUpdate action
- * @param columns The list of columns in the current table
- * @param referencedColumns The list of columns in the referenced table
- */
- public ForeignKeyBundle(String table, String onDelete, String onUpdate,
- List<String> columns, List<String> referencedColumns) {
- mTable = table;
- mOnDelete = onDelete;
- mOnUpdate = onUpdate;
- mColumns = columns;
- mReferencedColumns = referencedColumns;
- }
-
- /**
- * Returns the table name
- *
- * @return Returns the table name
- */
- public String getTable() {
- return mTable;
- }
-
- /**
- * Returns the SQLite foreign key action that will be performed when referenced row is deleted.
- *
- * @return The SQLite on delete action
- */
- public String getOnDelete() {
- return mOnDelete;
- }
-
- /**
- * Returns the SQLite foreign key action that will be performed when referenced row is updated.
- *
- * @return The SQLite on update action
- */
- public String getOnUpdate() {
- return mOnUpdate;
- }
-
- /**
- * Returns the ordered list of columns in the current table.
- *
- * @return The list of columns in the current entity.
- */
- public List<String> getColumns() {
- return mColumns;
- }
-
- /**
- * Returns the ordered list of columns in the referenced table.
- *
- * @return The list of columns in the referenced entity.
- */
- public List<String> getReferencedColumns() {
- return mReferencedColumns;
- }
-
- @Override
- public boolean isSchemaEqual(ForeignKeyBundle other) {
- if (mTable != null ? !mTable.equals(other.mTable) : other.mTable != null) return false;
- if (mOnDelete != null ? !mOnDelete.equals(other.mOnDelete) : other.mOnDelete != null) {
- return false;
- }
- if (mOnUpdate != null ? !mOnUpdate.equals(other.mOnUpdate) : other.mOnUpdate != null) {
- return false;
- }
- // order matters
- return mColumns.equals(other.mColumns) && mReferencedColumns.equals(
- other.mReferencedColumns);
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
deleted file mode 100644
index e991316..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/IndexBundle.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.util.List;
-
-/**
- * Data class that holds the schema information about a table Index.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IndexBundle implements SchemaEquality<IndexBundle> {
- // should match Index.kt
- public static final String DEFAULT_PREFIX = "index_";
- @SerializedName("name")
- private String mName;
- @SerializedName("unique")
- private boolean mUnique;
- @SerializedName("columnNames")
- private List<String> mColumnNames;
- @SerializedName("createSql")
- private String mCreateSql;
-
- public IndexBundle(String name, boolean unique, List<String> columnNames,
- String createSql) {
- mName = name;
- mUnique = unique;
- mColumnNames = columnNames;
- mCreateSql = createSql;
- }
-
- public String getName() {
- return mName;
- }
-
- public boolean isUnique() {
- return mUnique;
- }
-
- public List<String> getColumnNames() {
- return mColumnNames;
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public String create(String tableName) {
- return BundleUtil.replaceTableName(mCreateSql, tableName);
- }
-
- @Override
- public boolean isSchemaEqual(IndexBundle other) {
- if (mUnique != other.mUnique) return false;
- if (mName.startsWith(DEFAULT_PREFIX)) {
- if (!other.mName.startsWith(DEFAULT_PREFIX)) {
- return false;
- }
- } else if (other.mName.startsWith(DEFAULT_PREFIX)) {
- return false;
- } else if (!mName.equals(other.mName)) {
- return false;
- }
-
- // order matters
- if (mColumnNames != null ? !mColumnNames.equals(other.mColumnNames)
- : other.mColumnNames != null) {
- return false;
- }
- return true;
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
deleted file mode 100644
index 820aa7e..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.annotations.SerializedName;
-
-import java.util.List;
-
-/**
- * Data class that holds the schema information about a primary key.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class PrimaryKeyBundle implements SchemaEquality<PrimaryKeyBundle> {
- @SerializedName("columnNames")
- private List<String> mColumnNames;
- @SerializedName("autoGenerate")
- private boolean mAutoGenerate;
-
- public PrimaryKeyBundle(boolean autoGenerate, List<String> columnNames) {
- mColumnNames = columnNames;
- mAutoGenerate = autoGenerate;
- }
-
- public List<String> getColumnNames() {
- return mColumnNames;
- }
-
- public boolean isAutoGenerate() {
- return mAutoGenerate;
- }
-
- @Override
- public boolean isSchemaEqual(PrimaryKeyBundle other) {
- return mColumnNames.equals(other.mColumnNames) && mAutoGenerate == other.mAutoGenerate;
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
deleted file mode 100644
index af35e6f..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaBundle.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.annotations.SerializedName;
-
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Data class that holds the information about a database schema export.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SchemaBundle implements SchemaEquality<SchemaBundle> {
-
- @SerializedName("formatVersion")
- private int mFormatVersion;
- @SerializedName("database")
- private DatabaseBundle mDatabase;
-
- private static final Gson GSON;
- private static final String CHARSET = "UTF-8";
- public static final int LATEST_FORMAT = 1;
-
- static {
- GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
- }
-
- public SchemaBundle(int formatVersion, DatabaseBundle database) {
- mFormatVersion = formatVersion;
- mDatabase = database;
- }
-
- @SuppressWarnings("unused")
- public int getFormatVersion() {
- return mFormatVersion;
- }
-
- public DatabaseBundle getDatabase() {
- return mDatabase;
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static SchemaBundle deserialize(InputStream fis)
- throws UnsupportedEncodingException {
- InputStreamReader is = new InputStreamReader(fis, CHARSET);
- try {
- return GSON.fromJson(is, SchemaBundle.class);
- } finally {
- safeClose(is);
- safeClose(fis);
- }
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static void serialize(SchemaBundle bundle, File file) throws IOException {
- FileOutputStream fos = new FileOutputStream(file, false);
- OutputStreamWriter osw = new OutputStreamWriter(fos, CHARSET);
- try {
- GSON.toJson(bundle, osw);
- } finally {
- safeClose(osw);
- safeClose(fos);
- }
- }
-
- private static void safeClose(Closeable closeable) {
- if (closeable != null) {
- try {
- closeable.close();
- } catch (Throwable ignored) {
- }
- }
- }
-
- @Override
- public boolean isSchemaEqual(SchemaBundle other) {
- return SchemaEqualityUtil.checkSchemaEquality(mDatabase, other.mDatabase)
- && mFormatVersion == other.mFormatVersion;
- }
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java
deleted file mode 100644
index 59ea4b0..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEquality.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import android.support.annotation.RestrictTo;
-
-/**
- * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
- * columns in an entity does not have to match)
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-interface SchemaEquality<T> {
- boolean isSchemaEqual(T other);
-}
diff --git a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java b/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
deleted file mode 100644
index 65a7572..0000000
--- a/room/migration/src/main/java/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * utility class to run schema equality on collections.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class SchemaEqualityUtil {
- static <T, K extends SchemaEquality<K>> boolean checkSchemaEquality(
- @Nullable Map<T, K> map1, @Nullable Map<T, K> map2) {
- if (map1 == null) {
- return map2 == null;
- }
- if (map2 == null) {
- return false;
- }
- if (map1.size() != map2.size()) {
- return false;
- }
- for (Map.Entry<T, K> pair : map1.entrySet()) {
- if (!checkSchemaEquality(pair.getValue(), map2.get(pair.getKey()))) {
- return false;
- }
- }
- return true;
- }
-
- static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
- @Nullable List<K> list1, @Nullable List<K> list2) {
- if (list1 == null) {
- return list2 == null;
- }
- if (list2 == null) {
- return false;
- }
- if (list1.size() != list2.size()) {
- return false;
- }
- // we don't care this is n^2, small list + only used for testing.
- for (K item1 : list1) {
- // find matching item
- boolean matched = false;
- for (K item2 : list2) {
- if (checkSchemaEquality(item1, item2)) {
- matched = true;
- break;
- }
- }
- if (!matched) {
- return false;
- }
- }
- return true;
- }
-
- @SuppressWarnings("SimplifiableIfStatement")
- static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
- @Nullable K item1, @Nullable K item2) {
- if (item1 == null) {
- return item2 == null;
- }
- if (item2 == null) {
- return false;
- }
- return item1.isSchemaEqual(item2);
- }
-}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/BundleUtil.java b/room/migration/src/main/java/androidx/room/migration/bundle/BundleUtil.java
new file mode 100644
index 0000000..8eb4d10
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/BundleUtil.java
@@ -0,0 +1,36 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utility functions for bundling.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class BundleUtil {
+ /**
+ * Placeholder for table names in queries.
+ */
+ public static final String TABLE_NAME_PLACEHOLDER = "${TABLE_NAME}";
+
+ static String replaceTableName(String contents, String tableName) {
+ return contents.replace(TABLE_NAME_PLACEHOLDER, tableName);
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/DatabaseBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/DatabaseBundle.java
new file mode 100644
index 0000000..fa658bb
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/DatabaseBundle.java
@@ -0,0 +1,113 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data class that holds the schema information for a
+ * {@link androidx.room.Database Database}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class DatabaseBundle implements SchemaEquality<DatabaseBundle> {
+ @SerializedName("version")
+ private int mVersion;
+ @SerializedName("identityHash")
+ private String mIdentityHash;
+ @SerializedName("entities")
+ private List<EntityBundle> mEntities;
+ // then entity where we keep room information
+ @SerializedName("setupQueries")
+ private List<String> mSetupQueries;
+ private transient Map<String, EntityBundle> mEntitiesByTableName;
+
+ /**
+ * Creates a new database
+ * @param version Version
+ * @param identityHash Identity hash
+ * @param entities List of entities
+ */
+ public DatabaseBundle(int version, String identityHash, List<EntityBundle> entities,
+ List<String> setupQueries) {
+ mVersion = version;
+ mIdentityHash = identityHash;
+ mEntities = entities;
+ mSetupQueries = setupQueries;
+ }
+
+ /**
+ * @return The identity has of the Database.
+ */
+ public String getIdentityHash() {
+ return mIdentityHash;
+ }
+
+ /**
+ * @return The database version.
+ */
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /**
+ * @return List of entities.
+ */
+ public List<EntityBundle> getEntities() {
+ return mEntities;
+ }
+
+ /**
+ * @return Map of entities, keyed by table name.
+ */
+ @SuppressWarnings("unused")
+ public Map<String, EntityBundle> getEntitiesByTableName() {
+ if (mEntitiesByTableName == null) {
+ mEntitiesByTableName = new HashMap<>();
+ for (EntityBundle bundle : mEntities) {
+ mEntitiesByTableName.put(bundle.getTableName(), bundle);
+ }
+ }
+ return mEntitiesByTableName;
+ }
+
+ /**
+ * @return List of SQL queries to build this database from scratch.
+ */
+ public List<String> buildCreateQueries() {
+ List<String> result = new ArrayList<>();
+ for (EntityBundle entityBundle : mEntities) {
+ result.addAll(entityBundle.buildCreateQueries());
+ }
+ result.addAll(mSetupQueries);
+ return result;
+ }
+
+ @Override
+ public boolean isSchemaEqual(DatabaseBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(getEntitiesByTableName(),
+ other.getEntitiesByTableName());
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/EntityBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/EntityBundle.java
new file mode 100644
index 0000000..7fd836f
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/EntityBundle.java
@@ -0,0 +1,192 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import static androidx.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data class that holds the schema information about an
+ * {@link androidx.room.Entity Entity}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class EntityBundle implements SchemaEquality<EntityBundle> {
+
+ static final String NEW_TABLE_PREFIX = "_new_";
+
+ @SerializedName("tableName")
+ private String mTableName;
+ @SerializedName("createSql")
+ private String mCreateSql;
+ @SerializedName("fields")
+ private List<FieldBundle> mFields;
+ @SerializedName("primaryKey")
+ private PrimaryKeyBundle mPrimaryKey;
+ @SerializedName("indices")
+ private List<IndexBundle> mIndices;
+ @SerializedName("foreignKeys")
+ private List<ForeignKeyBundle> mForeignKeys;
+
+ private transient String mNewTableName;
+ private transient Map<String, FieldBundle> mFieldsByColumnName;
+
+ /**
+ * Creates a new bundle.
+ *
+ * @param tableName The table name.
+ * @param createSql Create query with the table name placeholder.
+ * @param fields The list of fields.
+ * @param primaryKey The primary key.
+ * @param indices The list of indices
+ * @param foreignKeys The list of foreign keys
+ */
+ public EntityBundle(String tableName, String createSql,
+ List<FieldBundle> fields,
+ PrimaryKeyBundle primaryKey,
+ List<IndexBundle> indices,
+ List<ForeignKeyBundle> foreignKeys) {
+ mTableName = tableName;
+ mCreateSql = createSql;
+ mFields = fields;
+ mPrimaryKey = primaryKey;
+ mIndices = indices;
+ mForeignKeys = foreignKeys;
+ }
+
+ /**
+ * @return The table name if it is created during a table schema modification.
+ */
+ public String getNewTableName() {
+ if (mNewTableName == null) {
+ mNewTableName = NEW_TABLE_PREFIX + mTableName;
+ }
+ return mNewTableName;
+ }
+
+ /**
+ * @return Map of fields keyed by their column names.
+ */
+ public Map<String, FieldBundle> getFieldsByColumnName() {
+ if (mFieldsByColumnName == null) {
+ mFieldsByColumnName = new HashMap<>();
+ for (FieldBundle bundle : mFields) {
+ mFieldsByColumnName.put(bundle.getColumnName(), bundle);
+ }
+ }
+ return mFieldsByColumnName;
+ }
+
+ /**
+ * @return The table name.
+ */
+ public String getTableName() {
+ return mTableName;
+ }
+
+ /**
+ * @return The create query with table name placeholder.
+ */
+ public String getCreateSql() {
+ return mCreateSql;
+ }
+
+ /**
+ * @return List of fields.
+ */
+ public List<FieldBundle> getFields() {
+ return mFields;
+ }
+
+ /**
+ * @return The primary key description.
+ */
+ public PrimaryKeyBundle getPrimaryKey() {
+ return mPrimaryKey;
+ }
+
+ /**
+ * @return List of indices.
+ */
+ public List<IndexBundle> getIndices() {
+ return mIndices;
+ }
+
+ /**
+ * @return List of foreign keys.
+ */
+ public List<ForeignKeyBundle> getForeignKeys() {
+ return mForeignKeys;
+ }
+
+ /**
+ * @return Create table SQL query that uses the actual table name.
+ */
+ public String createTable() {
+ return BundleUtil.replaceTableName(mCreateSql, getTableName());
+ }
+
+ /**
+ * @return Create table SQL query that uses the table name with "new" prefix.
+ */
+ public String createNewTable() {
+ return BundleUtil.replaceTableName(mCreateSql, getNewTableName());
+ }
+
+ /**
+ * @return Renames the table with {@link #getNewTableName()} to {@link #getTableName()}.
+ */
+ @NotNull
+ public String renameToOriginal() {
+ return "ALTER TABLE " + getNewTableName() + " RENAME TO " + getTableName();
+ }
+
+ /**
+ * @return Creates the list of SQL queries that are necessary to create this entity.
+ */
+ public Collection<String> buildCreateQueries() {
+ List<String> result = new ArrayList<>();
+ result.add(createTable());
+ for (IndexBundle indexBundle : mIndices) {
+ result.add(indexBundle.create(getTableName()));
+ }
+ return result;
+ }
+
+ @Override
+ public boolean isSchemaEqual(EntityBundle other) {
+ if (!mTableName.equals(other.mTableName)) {
+ return false;
+ }
+ return checkSchemaEquality(getFieldsByColumnName(), other.getFieldsByColumnName())
+ && checkSchemaEquality(mPrimaryKey, other.mPrimaryKey)
+ && checkSchemaEquality(mIndices, other.mIndices)
+ && checkSchemaEquality(mForeignKeys, other.mForeignKeys);
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/FieldBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/FieldBundle.java
new file mode 100644
index 0000000..c6612f4
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/FieldBundle.java
@@ -0,0 +1,72 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Data class that holds the schema information for an
+ * {@link androidx.room.Entity Entity} field.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class FieldBundle implements SchemaEquality<FieldBundle> {
+ @SerializedName("fieldPath")
+ private String mFieldPath;
+ @SerializedName("columnName")
+ private String mColumnName;
+ @SerializedName("affinity")
+ private String mAffinity;
+ @SerializedName("notNull")
+ private boolean mNonNull;
+
+ public FieldBundle(String fieldPath, String columnName, String affinity, boolean nonNull) {
+ mFieldPath = fieldPath;
+ mColumnName = columnName;
+ mAffinity = affinity;
+ mNonNull = nonNull;
+ }
+
+ public String getFieldPath() {
+ return mFieldPath;
+ }
+
+ public String getColumnName() {
+ return mColumnName;
+ }
+
+ public String getAffinity() {
+ return mAffinity;
+ }
+
+ public boolean isNonNull() {
+ return mNonNull;
+ }
+
+ @Override
+ public boolean isSchemaEqual(FieldBundle other) {
+ if (mNonNull != other.mNonNull) return false;
+ if (mColumnName != null ? !mColumnName.equals(other.mColumnName)
+ : other.mColumnName != null) {
+ return false;
+ }
+ return mAffinity != null ? mAffinity.equals(other.mAffinity) : other.mAffinity == null;
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/ForeignKeyBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/ForeignKeyBundle.java
new file mode 100644
index 0000000..3d45900
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/ForeignKeyBundle.java
@@ -0,0 +1,119 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+/**
+ * Holds the information about a foreign key reference.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ForeignKeyBundle implements SchemaEquality<ForeignKeyBundle> {
+ @SerializedName("table")
+ private String mTable;
+ @SerializedName("onDelete")
+ private String mOnDelete;
+ @SerializedName("onUpdate")
+ private String mOnUpdate;
+ @SerializedName("columns")
+ private List<String> mColumns;
+ @SerializedName("referencedColumns")
+ private List<String> mReferencedColumns;
+
+ /**
+ * Creates a foreign key bundle with the given parameters.
+ *
+ * @param table The target table
+ * @param onDelete OnDelete action
+ * @param onUpdate OnUpdate action
+ * @param columns The list of columns in the current table
+ * @param referencedColumns The list of columns in the referenced table
+ */
+ public ForeignKeyBundle(String table, String onDelete, String onUpdate,
+ List<String> columns, List<String> referencedColumns) {
+ mTable = table;
+ mOnDelete = onDelete;
+ mOnUpdate = onUpdate;
+ mColumns = columns;
+ mReferencedColumns = referencedColumns;
+ }
+
+ /**
+ * Returns the table name
+ *
+ * @return Returns the table name
+ */
+ public String getTable() {
+ return mTable;
+ }
+
+ /**
+ * Returns the SQLite foreign key action that will be performed when referenced row is deleted.
+ *
+ * @return The SQLite on delete action
+ */
+ public String getOnDelete() {
+ return mOnDelete;
+ }
+
+ /**
+ * Returns the SQLite foreign key action that will be performed when referenced row is updated.
+ *
+ * @return The SQLite on update action
+ */
+ public String getOnUpdate() {
+ return mOnUpdate;
+ }
+
+ /**
+ * Returns the ordered list of columns in the current table.
+ *
+ * @return The list of columns in the current entity.
+ */
+ public List<String> getColumns() {
+ return mColumns;
+ }
+
+ /**
+ * Returns the ordered list of columns in the referenced table.
+ *
+ * @return The list of columns in the referenced entity.
+ */
+ public List<String> getReferencedColumns() {
+ return mReferencedColumns;
+ }
+
+ @Override
+ public boolean isSchemaEqual(ForeignKeyBundle other) {
+ if (mTable != null ? !mTable.equals(other.mTable) : other.mTable != null) return false;
+ if (mOnDelete != null ? !mOnDelete.equals(other.mOnDelete) : other.mOnDelete != null) {
+ return false;
+ }
+ if (mOnUpdate != null ? !mOnUpdate.equals(other.mOnUpdate) : other.mOnUpdate != null) {
+ return false;
+ }
+ // order matters
+ return mColumns.equals(other.mColumns) && mReferencedColumns.equals(
+ other.mReferencedColumns);
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/IndexBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/IndexBundle.java
new file mode 100644
index 0000000..baf15ab
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/IndexBundle.java
@@ -0,0 +1,91 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+/**
+ * Data class that holds the schema information about a table Index.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class IndexBundle implements SchemaEquality<IndexBundle> {
+ // should match Index.kt
+ public static final String DEFAULT_PREFIX = "index_";
+ @SerializedName("name")
+ private String mName;
+ @SerializedName("unique")
+ private boolean mUnique;
+ @SerializedName("columnNames")
+ private List<String> mColumnNames;
+ @SerializedName("createSql")
+ private String mCreateSql;
+
+ public IndexBundle(String name, boolean unique, List<String> columnNames,
+ String createSql) {
+ mName = name;
+ mUnique = unique;
+ mColumnNames = columnNames;
+ mCreateSql = createSql;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public boolean isUnique() {
+ return mUnique;
+ }
+
+ public List<String> getColumnNames() {
+ return mColumnNames;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public String create(String tableName) {
+ return BundleUtil.replaceTableName(mCreateSql, tableName);
+ }
+
+ @Override
+ public boolean isSchemaEqual(IndexBundle other) {
+ if (mUnique != other.mUnique) return false;
+ if (mName.startsWith(DEFAULT_PREFIX)) {
+ if (!other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ }
+ } else if (other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ } else if (!mName.equals(other.mName)) {
+ return false;
+ }
+
+ // order matters
+ if (mColumnNames != null ? !mColumnNames.equals(other.mColumnNames)
+ : other.mColumnNames != null) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/PrimaryKeyBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/PrimaryKeyBundle.java
new file mode 100644
index 0000000..a77da7d
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/PrimaryKeyBundle.java
@@ -0,0 +1,54 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+/**
+ * Data class that holds the schema information about a primary key.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class PrimaryKeyBundle implements SchemaEquality<PrimaryKeyBundle> {
+ @SerializedName("columnNames")
+ private List<String> mColumnNames;
+ @SerializedName("autoGenerate")
+ private boolean mAutoGenerate;
+
+ public PrimaryKeyBundle(boolean autoGenerate, List<String> columnNames) {
+ mColumnNames = columnNames;
+ mAutoGenerate = autoGenerate;
+ }
+
+ public List<String> getColumnNames() {
+ return mColumnNames;
+ }
+
+ public boolean isAutoGenerate() {
+ return mAutoGenerate;
+ }
+
+ @Override
+ public boolean isSchemaEqual(PrimaryKeyBundle other) {
+ return mColumnNames.equals(other.mColumnNames) && mAutoGenerate == other.mAutoGenerate;
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.java b/room/migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.java
new file mode 100644
index 0000000..8bc3d3d
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/SchemaBundle.java
@@ -0,0 +1,113 @@
+/*
+ * 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Data class that holds the information about a database schema export.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaBundle implements SchemaEquality<SchemaBundle> {
+
+ @SerializedName("formatVersion")
+ private int mFormatVersion;
+ @SerializedName("database")
+ private DatabaseBundle mDatabase;
+
+ private static final Gson GSON;
+ private static final String CHARSET = "UTF-8";
+ public static final int LATEST_FORMAT = 1;
+
+ static {
+ GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+ }
+
+ public SchemaBundle(int formatVersion, DatabaseBundle database) {
+ mFormatVersion = formatVersion;
+ mDatabase = database;
+ }
+
+ @SuppressWarnings("unused")
+ public int getFormatVersion() {
+ return mFormatVersion;
+ }
+
+ public DatabaseBundle getDatabase() {
+ return mDatabase;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static SchemaBundle deserialize(InputStream fis)
+ throws UnsupportedEncodingException {
+ InputStreamReader is = new InputStreamReader(fis, CHARSET);
+ try {
+ return GSON.fromJson(is, SchemaBundle.class);
+ } finally {
+ safeClose(is);
+ safeClose(fis);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static void serialize(SchemaBundle bundle, File file) throws IOException {
+ FileOutputStream fos = new FileOutputStream(file, false);
+ OutputStreamWriter osw = new OutputStreamWriter(fos, CHARSET);
+ try {
+ GSON.toJson(bundle, osw);
+ } finally {
+ safeClose(osw);
+ safeClose(fos);
+ }
+ }
+
+ private static void safeClose(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+
+ @Override
+ public boolean isSchemaEqual(SchemaBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(mDatabase, other.mDatabase)
+ && mFormatVersion == other.mFormatVersion;
+ }
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/SchemaEquality.java b/room/migration/src/main/java/androidx/room/migration/bundle/SchemaEquality.java
new file mode 100644
index 0000000..d300fea
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/SchemaEquality.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
+ * columns in an entity does not have to match)
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SchemaEquality<T> {
+ boolean isSchemaEqual(T other);
+}
diff --git a/room/migration/src/main/java/androidx/room/migration/bundle/SchemaEqualityUtil.java b/room/migration/src/main/java/androidx/room/migration/bundle/SchemaEqualityUtil.java
new file mode 100644
index 0000000..799c3d8
--- /dev/null
+++ b/room/migration/src/main/java/androidx/room/migration/bundle/SchemaEqualityUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * utility class to run schema equality on collections.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SchemaEqualityUtil {
+ static <T, K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable Map<T, K> map1, @Nullable Map<T, K> map2) {
+ if (map1 == null) {
+ return map2 == null;
+ }
+ if (map2 == null) {
+ return false;
+ }
+ if (map1.size() != map2.size()) {
+ return false;
+ }
+ for (Map.Entry<T, K> pair : map1.entrySet()) {
+ if (!checkSchemaEquality(pair.getValue(), map2.get(pair.getKey()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable List<K> list1, @Nullable List<K> list2) {
+ if (list1 == null) {
+ return list2 == null;
+ }
+ if (list2 == null) {
+ return false;
+ }
+ if (list1.size() != list2.size()) {
+ return false;
+ }
+ // we don't care this is n^2, small list + only used for testing.
+ for (K item1 : list1) {
+ // find matching item
+ boolean matched = false;
+ for (K item2 : list2) {
+ if (checkSchemaEquality(item1, item2)) {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable K item1, @Nullable K item2) {
+ if (item1 == null) {
+ return item2 == null;
+ }
+ if (item2 == null) {
+ return false;
+ }
+ return item1.isSchemaEqual(item2);
+ }
+}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
deleted file mode 100644
index 4b4df8b..0000000
--- a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import static java.util.Arrays.asList;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Collections;
-
-@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
-@RunWith(JUnit4.class)
-public class EntityBundleTest {
- @Test
- public void schemaEquality_same_equal() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- asList(createFieldBundle("foo"), createFieldBundle("bar")),
- new PrimaryKeyBundle(false, asList("foo")),
- asList(createIndexBundle("foo")),
- asList(createForeignKeyBundle("bar", "foo")));
-
- EntityBundle other = new EntityBundle("foo", "sq",
- asList(createFieldBundle("foo"), createFieldBundle("bar")),
- new PrimaryKeyBundle(false, asList("foo")),
- asList(createIndexBundle("foo")),
- asList(createForeignKeyBundle("bar", "foo")));
-
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_reorderedFields_equal() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- asList(createFieldBundle("foo"), createFieldBundle("bar")),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- Collections.<ForeignKeyBundle>emptyList());
-
- EntityBundle other = new EntityBundle("foo", "sq",
- asList(createFieldBundle("bar"), createFieldBundle("foo")),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- Collections.<ForeignKeyBundle>emptyList());
-
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffFields_notEqual() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- asList(createFieldBundle("foo"), createFieldBundle("bar")),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- Collections.<ForeignKeyBundle>emptyList());
-
- EntityBundle other = new EntityBundle("foo", "sq",
- asList(createFieldBundle("foo2"), createFieldBundle("bar")),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- Collections.<ForeignKeyBundle>emptyList());
-
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_reorderedForeignKeys_equal() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- asList(createForeignKeyBundle("x", "y"),
- createForeignKeyBundle("bar", "foo")));
-
- EntityBundle other = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- asList(createForeignKeyBundle("bar", "foo"),
- createForeignKeyBundle("x", "y")));
-
-
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffForeignKeys_notEqual() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- asList(createForeignKeyBundle("bar", "foo")));
-
- EntityBundle other = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- Collections.<IndexBundle>emptyList(),
- asList(createForeignKeyBundle("bar2", "foo")));
-
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_reorderedIndices_equal() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- asList(createIndexBundle("foo"), createIndexBundle("baz")),
- Collections.<ForeignKeyBundle>emptyList());
-
- EntityBundle other = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- asList(createIndexBundle("baz"), createIndexBundle("foo")),
- Collections.<ForeignKeyBundle>emptyList());
-
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffIndices_notEqual() {
- EntityBundle bundle = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- asList(createIndexBundle("foo")),
- Collections.<ForeignKeyBundle>emptyList());
-
- EntityBundle other = new EntityBundle("foo", "sq",
- Collections.<FieldBundle>emptyList(),
- new PrimaryKeyBundle(false, asList("foo")),
- asList(createIndexBundle("foo2")),
- Collections.<ForeignKeyBundle>emptyList());
-
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- private FieldBundle createFieldBundle(String name) {
- return new FieldBundle("foo", name, "text", false);
- }
-
- private IndexBundle createIndexBundle(String colName) {
- return new IndexBundle("ind_" + colName, false,
- asList(colName), "create");
- }
-
- private ForeignKeyBundle createForeignKeyBundle(String targetTable, String column) {
- return new ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
- asList(column), asList(column));
- }
-}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
deleted file mode 100644
index eac4477..0000000
--- a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class FieldBundleTest {
- @Test
- public void schemaEquality_same_equal() {
- FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
- FieldBundle copy = new FieldBundle("foo", "foo", "text", false);
- assertThat(bundle.isSchemaEqual(copy), is(true));
- }
-
- @Test
- public void schemaEquality_diffNonNull_notEqual() {
- FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
- FieldBundle copy = new FieldBundle("foo", "foo", "text", true);
- assertThat(bundle.isSchemaEqual(copy), is(false));
- }
-
- @Test
- public void schemaEquality_diffColumnName_notEqual() {
- FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
- FieldBundle copy = new FieldBundle("foo", "foo2", "text", true);
- assertThat(bundle.isSchemaEqual(copy), is(false));
- }
-
- @Test
- public void schemaEquality_diffAffinity_notEqual() {
- FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
- FieldBundle copy = new FieldBundle("foo", "foo2", "int", false);
- assertThat(bundle.isSchemaEqual(copy), is(false));
- }
-
- @Test
- public void schemaEquality_diffPath_equal() {
- FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
- FieldBundle copy = new FieldBundle("foo>bar", "foo", "text", false);
- assertThat(bundle.isSchemaEqual(copy), is(true));
- }
-}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
deleted file mode 100644
index be1b81e..0000000
--- a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-
-@RunWith(JUnit4.class)
-public class ForeignKeyBundleTest {
- @Test
- public void schemaEquality_same_equal() {
- ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffTable_notEqual() {
- ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- ForeignKeyBundle other = new ForeignKeyBundle("table2", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffOnDelete_notEqual() {
- ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete2",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffOnUpdate_notEqual() {
- ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
- "onUpdate2", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffSrcOrder_notEqual() {
- ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col2", "col1"),
- Arrays.asList("target1", "target2"));
- ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffTargetOrder_notEqual() {
- ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target1", "target2"));
- ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
- "onUpdate", Arrays.asList("col1", "col2"),
- Arrays.asList("target2", "target1"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
deleted file mode 100644
index aa7230f..0000000
--- a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-
-@RunWith(JUnit4.class)
-public class IndexBundleTest {
- @Test
- public void schemaEquality_same_equal() {
- IndexBundle bundle = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql");
- IndexBundle other = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql");
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffName_notEqual() {
- IndexBundle bundle = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql");
- IndexBundle other = new IndexBundle("index3", false,
- Arrays.asList("col1", "col2"), "sql");
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffGenericName_equal() {
- IndexBundle bundle = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
- Arrays.asList("col1", "col2"), "sql");
- IndexBundle other = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
- Arrays.asList("col1", "col2"), "sql");
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffUnique_notEqual() {
- IndexBundle bundle = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql");
- IndexBundle other = new IndexBundle("index1", true,
- Arrays.asList("col1", "col2"), "sql");
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffColumns_notEqual() {
- IndexBundle bundle = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql");
- IndexBundle other = new IndexBundle("index1", false,
- Arrays.asList("col2", "col1"), "sql");
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffSql_equal() {
- IndexBundle bundle = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql");
- IndexBundle other = new IndexBundle("index1", false,
- Arrays.asList("col1", "col2"), "sql22");
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-}
diff --git a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java b/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
deleted file mode 100644
index 3b9e464..0000000
--- a/room/migration/src/test/java/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 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.migration.bundle;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-
-@RunWith(JUnit4.class)
-public class PrimaryKeyBundleTest {
- @Test
- public void schemaEquality_same_equal() {
- PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
- Arrays.asList("foo", "bar"));
- PrimaryKeyBundle other = new PrimaryKeyBundle(true,
- Arrays.asList("foo", "bar"));
- assertThat(bundle.isSchemaEqual(other), is(true));
- }
-
- @Test
- public void schemaEquality_diffAutoGen_notEqual() {
- PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
- Arrays.asList("foo", "bar"));
- PrimaryKeyBundle other = new PrimaryKeyBundle(false,
- Arrays.asList("foo", "bar"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffColumns_notEqual() {
- PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
- Arrays.asList("foo", "baz"));
- PrimaryKeyBundle other = new PrimaryKeyBundle(true,
- Arrays.asList("foo", "bar"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-
- @Test
- public void schemaEquality_diffColumnOrder_notEqual() {
- PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
- Arrays.asList("foo", "bar"));
- PrimaryKeyBundle other = new PrimaryKeyBundle(true,
- Arrays.asList("bar", "foo"));
- assertThat(bundle.isSchemaEqual(other), is(false));
- }
-}
diff --git a/room/migration/src/test/java/androidx/room/migration/bundle/EntityBundleTest.java b/room/migration/src/test/java/androidx/room/migration/bundle/EntityBundleTest.java
new file mode 100644
index 0000000..307f9d6
--- /dev/null
+++ b/room/migration/src/test/java/androidx/room/migration/bundle/EntityBundleTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class EntityBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_reorderedFields_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("bar"), createFieldBundle("foo")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffFields_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo2"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedForeignKeys_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("x", "y"),
+ createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo"),
+ createForeignKeyBundle("x", "y")));
+
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffForeignKeys_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar2", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedIndices_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo"), createIndexBundle("baz")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("baz"), createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffIndices_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo2")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ private FieldBundle createFieldBundle(String name) {
+ return new FieldBundle("foo", name, "text", false);
+ }
+
+ private IndexBundle createIndexBundle(String colName) {
+ return new IndexBundle("ind_" + colName, false,
+ asList(colName), "create");
+ }
+
+ private ForeignKeyBundle createForeignKeyBundle(String targetTable, String column) {
+ return new ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
+ asList(column), asList(column));
+ }
+}
diff --git a/room/migration/src/test/java/androidx/room/migration/bundle/FieldBundleTest.java b/room/migration/src/test/java/androidx/room/migration/bundle/FieldBundleTest.java
new file mode 100644
index 0000000..e48114a
--- /dev/null
+++ b/room/migration/src/test/java/androidx/room/migration/bundle/FieldBundleTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffNonNull_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnName_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffAffinity_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "int", false);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffPath_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo>bar", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+}
diff --git a/room/migration/src/test/java/androidx/room/migration/bundle/ForeignKeyBundleTest.java b/room/migration/src/test/java/androidx/room/migration/bundle/ForeignKeyBundleTest.java
new file mode 100644
index 0000000..198e34f
--- /dev/null
+++ b/room/migration/src/test/java/androidx/room/migration/bundle/ForeignKeyBundleTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class ForeignKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffTable_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table2", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnDelete_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete2",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnUpdate_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate2", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSrcOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col2", "col1"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffTargetOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target2", "target1"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/room/migration/src/test/java/androidx/room/migration/bundle/IndexBundleTest.java b/room/migration/src/test/java/androidx/room/migration/bundle/IndexBundleTest.java
new file mode 100644
index 0000000..82d66e2
--- /dev/null
+++ b/room/migration/src/test/java/androidx/room/migration/bundle/IndexBundleTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class IndexBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffName_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index3", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffGenericName_equal() {
+ IndexBundle bundle = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffUnique_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", true,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col2", "col1"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSql_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql22");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+}
diff --git a/room/migration/src/test/java/androidx/room/migration/bundle/PrimaryKeyBundleTest.java b/room/migration/src/test/java/androidx/room/migration/bundle/PrimaryKeyBundleTest.java
new file mode 100644
index 0000000..bae3060
--- /dev/null
+++ b/room/migration/src/test/java/androidx/room/migration/bundle/PrimaryKeyBundleTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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 androidx.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class PrimaryKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffAutoGen_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(false,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "baz"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnOrder_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("bar", "foo"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index 17948ee..cfd667d 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -1,21 +1,21 @@
-package android.arch.persistence.room {
+package androidx.room {
public class DatabaseConfiguration {
method public boolean isMigrationRequiredFrom(int);
field public final boolean allowMainThreadQueries;
- field public final java.util.List<android.arch.persistence.room.RoomDatabase.Callback> callbacks;
+ field public final java.util.List<androidx.room.RoomDatabase.Callback> callbacks;
field public final android.content.Context context;
- field public final android.arch.persistence.room.RoomDatabase.JournalMode journalMode;
- field public final android.arch.persistence.room.RoomDatabase.MigrationContainer migrationContainer;
+ field public final androidx.room.RoomDatabase.JournalMode journalMode;
+ field public final androidx.room.RoomDatabase.MigrationContainer migrationContainer;
field public final java.lang.String name;
field public final boolean requireMigration;
- field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
}
public class InvalidationTracker {
- method public void addObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ method public void addObserver(androidx.room.InvalidationTracker.Observer);
method public void refreshVersionsAsync();
- method public void removeObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ method public void removeObserver(androidx.room.InvalidationTracker.Observer);
}
public static abstract class InvalidationTracker.Observer {
@@ -26,8 +26,8 @@
public class Room {
ctor public Room();
- method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context, java.lang.Class<T>, java.lang.String);
- method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context, java.lang.Class<T>);
+ method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context, java.lang.Class<T>, java.lang.String);
+ method public static <T extends androidx.room.RoomDatabase> androidx.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context, java.lang.Class<T>);
field public static final java.lang.String MASTER_TABLE_NAME = "room_master_table";
}
@@ -36,63 +36,63 @@
method public void beginTransaction();
method public abstract void clearAllTables();
method public void close();
- method public android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
- method protected abstract android.arch.persistence.room.InvalidationTracker createInvalidationTracker();
- method protected abstract android.arch.persistence.db.SupportSQLiteOpenHelper createOpenHelper(android.arch.persistence.room.DatabaseConfiguration);
+ method public androidx.sqlite.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method protected abstract androidx.room.InvalidationTracker createInvalidationTracker();
+ method protected abstract androidx.sqlite.db.SupportSQLiteOpenHelper createOpenHelper(androidx.room.DatabaseConfiguration);
method public void endTransaction();
- method public android.arch.persistence.room.InvalidationTracker getInvalidationTracker();
- method public android.arch.persistence.db.SupportSQLiteOpenHelper getOpenHelper();
+ method public androidx.room.InvalidationTracker getInvalidationTracker();
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper getOpenHelper();
method public boolean inTransaction();
- method public void init(android.arch.persistence.room.DatabaseConfiguration);
- method protected void internalInitInvalidationTracker(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void init(androidx.room.DatabaseConfiguration);
+ method protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase);
method public boolean isOpen();
method public android.database.Cursor query(java.lang.String, java.lang.Object[]);
- method public android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery);
method public void runInTransaction(java.lang.Runnable);
method public <V> V runInTransaction(java.util.concurrent.Callable<V>);
method public void setTransactionSuccessful();
- field protected java.util.List<android.arch.persistence.room.RoomDatabase.Callback> mCallbacks;
- field protected volatile android.arch.persistence.db.SupportSQLiteDatabase mDatabase;
+ field protected java.util.List<androidx.room.RoomDatabase.Callback> mCallbacks;
+ field protected volatile androidx.sqlite.db.SupportSQLiteDatabase mDatabase;
}
- public static class RoomDatabase.Builder<T extends android.arch.persistence.room.RoomDatabase> {
- method public android.arch.persistence.room.RoomDatabase.Builder<T> addCallback(android.arch.persistence.room.RoomDatabase.Callback);
- method public android.arch.persistence.room.RoomDatabase.Builder<T> addMigrations(android.arch.persistence.room.migration.Migration...);
- method public android.arch.persistence.room.RoomDatabase.Builder<T> allowMainThreadQueries();
+ public static class RoomDatabase.Builder<T extends androidx.room.RoomDatabase> {
+ method public androidx.room.RoomDatabase.Builder<T> addCallback(androidx.room.RoomDatabase.Callback);
+ method public androidx.room.RoomDatabase.Builder<T> addMigrations(androidx.room.migration.Migration...);
+ method public androidx.room.RoomDatabase.Builder<T> allowMainThreadQueries();
method public T build();
- method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigration();
- method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(java.lang.Integer...);
- method public android.arch.persistence.room.RoomDatabase.Builder<T> openHelperFactory(android.arch.persistence.db.SupportSQLiteOpenHelper.Factory);
- method public android.arch.persistence.room.RoomDatabase.Builder<T> setJournalMode(android.arch.persistence.room.RoomDatabase.JournalMode);
+ method public androidx.room.RoomDatabase.Builder<T> fallbackToDestructiveMigration();
+ method public androidx.room.RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(int...);
+ method public androidx.room.RoomDatabase.Builder<T> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory);
+ method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode);
}
public static abstract class RoomDatabase.Callback {
ctor public RoomDatabase.Callback();
- method public void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
- method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onCreate(androidx.sqlite.db.SupportSQLiteDatabase);
+ method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase);
}
public static final class RoomDatabase.JournalMode extends java.lang.Enum {
- method public static android.arch.persistence.room.RoomDatabase.JournalMode valueOf(java.lang.String);
- method public static final android.arch.persistence.room.RoomDatabase.JournalMode[] values();
- enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode AUTOMATIC;
- enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode TRUNCATE;
- enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode WRITE_AHEAD_LOGGING;
+ method public static androidx.room.RoomDatabase.JournalMode valueOf(java.lang.String);
+ method public static final androidx.room.RoomDatabase.JournalMode[] values();
+ enum_constant public static final androidx.room.RoomDatabase.JournalMode AUTOMATIC;
+ enum_constant public static final androidx.room.RoomDatabase.JournalMode TRUNCATE;
+ enum_constant public static final androidx.room.RoomDatabase.JournalMode WRITE_AHEAD_LOGGING;
}
public static class RoomDatabase.MigrationContainer {
ctor public RoomDatabase.MigrationContainer();
- method public void addMigrations(android.arch.persistence.room.migration.Migration...);
- method public java.util.List<android.arch.persistence.room.migration.Migration> findMigrationPath(int, int);
+ method public void addMigrations(androidx.room.migration.Migration...);
+ method public java.util.List<androidx.room.migration.Migration> findMigrationPath(int, int);
}
}
-package android.arch.persistence.room.migration {
+package androidx.room.migration {
public abstract class Migration {
ctor public Migration(int, int);
- method public abstract void migrate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public abstract void migrate(androidx.sqlite.db.SupportSQLiteDatabase);
field public final int endVersion;
field public final int startVersion;
}
diff --git a/room/runtime/api/1.0.0.txt b/room/runtime/api_legacy/1.0.0.txt
similarity index 100%
rename from room/runtime/api/1.0.0.txt
rename to room/runtime/api_legacy/1.0.0.txt
diff --git a/room/runtime/api_legacy/1.1.0.txt b/room/runtime/api_legacy/1.1.0.txt
new file mode 100644
index 0000000..5a4f55b
--- /dev/null
+++ b/room/runtime/api_legacy/1.1.0.txt
@@ -0,0 +1,101 @@
+package android.arch.persistence.room {
+
+ public class DatabaseConfiguration {
+ method public boolean isMigrationRequiredFrom(int);
+ field public final boolean allowMainThreadQueries;
+ field public final java.util.List<android.arch.persistence.room.RoomDatabase.Callback> callbacks;
+ field public final android.content.Context context;
+ field public final android.arch.persistence.room.RoomDatabase.JournalMode journalMode;
+ field public final android.arch.persistence.room.RoomDatabase.MigrationContainer migrationContainer;
+ field public final java.lang.String name;
+ field public final boolean requireMigration;
+ field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ }
+
+ public class InvalidationTracker {
+ method public void addObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ method public void refreshVersionsAsync();
+ method public void removeObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ }
+
+ public static abstract class InvalidationTracker.Observer {
+ ctor protected InvalidationTracker.Observer(java.lang.String, java.lang.String...);
+ ctor public InvalidationTracker.Observer(java.lang.String[]);
+ method public abstract void onInvalidated(java.util.Set<java.lang.String>);
+ }
+
+ public class Room {
+ ctor public Room();
+ method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context, java.lang.Class<T>, java.lang.String);
+ method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context, java.lang.Class<T>);
+ field public static final java.lang.String MASTER_TABLE_NAME = "room_master_table";
+ }
+
+ public abstract class RoomDatabase {
+ ctor public RoomDatabase();
+ method public void beginTransaction();
+ method public abstract void clearAllTables();
+ method public void close();
+ method public android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method protected abstract android.arch.persistence.room.InvalidationTracker createInvalidationTracker();
+ method protected abstract android.arch.persistence.db.SupportSQLiteOpenHelper createOpenHelper(android.arch.persistence.room.DatabaseConfiguration);
+ method public void endTransaction();
+ method public android.arch.persistence.room.InvalidationTracker getInvalidationTracker();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper getOpenHelper();
+ method public boolean inTransaction();
+ method public void init(android.arch.persistence.room.DatabaseConfiguration);
+ method protected void internalInitInvalidationTracker(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public boolean isOpen();
+ method public android.database.Cursor query(java.lang.String, java.lang.Object[]);
+ method public android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public void runInTransaction(java.lang.Runnable);
+ method public <V> V runInTransaction(java.util.concurrent.Callable<V>);
+ method public void setTransactionSuccessful();
+ field protected java.util.List<android.arch.persistence.room.RoomDatabase.Callback> mCallbacks;
+ field protected volatile android.arch.persistence.db.SupportSQLiteDatabase mDatabase;
+ }
+
+ public static class RoomDatabase.Builder<T extends android.arch.persistence.room.RoomDatabase> {
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> addCallback(android.arch.persistence.room.RoomDatabase.Callback);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> addMigrations(android.arch.persistence.room.migration.Migration...);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> allowMainThreadQueries();
+ method public T build();
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigration();
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(int...);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> openHelperFactory(android.arch.persistence.db.SupportSQLiteOpenHelper.Factory);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> setJournalMode(android.arch.persistence.room.RoomDatabase.JournalMode);
+ }
+
+ public static abstract class RoomDatabase.Callback {
+ ctor public RoomDatabase.Callback();
+ method public void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ }
+
+ public static final class RoomDatabase.JournalMode extends java.lang.Enum {
+ method public static android.arch.persistence.room.RoomDatabase.JournalMode valueOf(java.lang.String);
+ method public static final android.arch.persistence.room.RoomDatabase.JournalMode[] values();
+ enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode AUTOMATIC;
+ enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode TRUNCATE;
+ enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode WRITE_AHEAD_LOGGING;
+ }
+
+ public static class RoomDatabase.MigrationContainer {
+ ctor public RoomDatabase.MigrationContainer();
+ method public void addMigrations(android.arch.persistence.room.migration.Migration...);
+ method public java.util.List<android.arch.persistence.room.migration.Migration> findMigrationPath(int, int);
+ }
+
+}
+
+package android.arch.persistence.room.migration {
+
+ public abstract class Migration {
+ ctor public Migration(int, int);
+ method public abstract void migrate(android.arch.persistence.db.SupportSQLiteDatabase);
+ field public final int endVersion;
+ field public final int startVersion;
+ }
+
+}
+
diff --git a/room/runtime/api_legacy/current.txt b/room/runtime/api_legacy/current.txt
new file mode 100644
index 0000000..5a4f55b
--- /dev/null
+++ b/room/runtime/api_legacy/current.txt
@@ -0,0 +1,101 @@
+package android.arch.persistence.room {
+
+ public class DatabaseConfiguration {
+ method public boolean isMigrationRequiredFrom(int);
+ field public final boolean allowMainThreadQueries;
+ field public final java.util.List<android.arch.persistence.room.RoomDatabase.Callback> callbacks;
+ field public final android.content.Context context;
+ field public final android.arch.persistence.room.RoomDatabase.JournalMode journalMode;
+ field public final android.arch.persistence.room.RoomDatabase.MigrationContainer migrationContainer;
+ field public final java.lang.String name;
+ field public final boolean requireMigration;
+ field public final android.arch.persistence.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ }
+
+ public class InvalidationTracker {
+ method public void addObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ method public void refreshVersionsAsync();
+ method public void removeObserver(android.arch.persistence.room.InvalidationTracker.Observer);
+ }
+
+ public static abstract class InvalidationTracker.Observer {
+ ctor protected InvalidationTracker.Observer(java.lang.String, java.lang.String...);
+ ctor public InvalidationTracker.Observer(java.lang.String[]);
+ method public abstract void onInvalidated(java.util.Set<java.lang.String>);
+ }
+
+ public class Room {
+ ctor public Room();
+ method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> databaseBuilder(android.content.Context, java.lang.Class<T>, java.lang.String);
+ method public static <T extends android.arch.persistence.room.RoomDatabase> android.arch.persistence.room.RoomDatabase.Builder<T> inMemoryDatabaseBuilder(android.content.Context, java.lang.Class<T>);
+ field public static final java.lang.String MASTER_TABLE_NAME = "room_master_table";
+ }
+
+ public abstract class RoomDatabase {
+ ctor public RoomDatabase();
+ method public void beginTransaction();
+ method public abstract void clearAllTables();
+ method public void close();
+ method public android.arch.persistence.db.SupportSQLiteStatement compileStatement(java.lang.String);
+ method protected abstract android.arch.persistence.room.InvalidationTracker createInvalidationTracker();
+ method protected abstract android.arch.persistence.db.SupportSQLiteOpenHelper createOpenHelper(android.arch.persistence.room.DatabaseConfiguration);
+ method public void endTransaction();
+ method public android.arch.persistence.room.InvalidationTracker getInvalidationTracker();
+ method public android.arch.persistence.db.SupportSQLiteOpenHelper getOpenHelper();
+ method public boolean inTransaction();
+ method public void init(android.arch.persistence.room.DatabaseConfiguration);
+ method protected void internalInitInvalidationTracker(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public boolean isOpen();
+ method public android.database.Cursor query(java.lang.String, java.lang.Object[]);
+ method public android.database.Cursor query(android.arch.persistence.db.SupportSQLiteQuery);
+ method public void runInTransaction(java.lang.Runnable);
+ method public <V> V runInTransaction(java.util.concurrent.Callable<V>);
+ method public void setTransactionSuccessful();
+ field protected java.util.List<android.arch.persistence.room.RoomDatabase.Callback> mCallbacks;
+ field protected volatile android.arch.persistence.db.SupportSQLiteDatabase mDatabase;
+ }
+
+ public static class RoomDatabase.Builder<T extends android.arch.persistence.room.RoomDatabase> {
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> addCallback(android.arch.persistence.room.RoomDatabase.Callback);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> addMigrations(android.arch.persistence.room.migration.Migration...);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> allowMainThreadQueries();
+ method public T build();
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigration();
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(int...);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> openHelperFactory(android.arch.persistence.db.SupportSQLiteOpenHelper.Factory);
+ method public android.arch.persistence.room.RoomDatabase.Builder<T> setJournalMode(android.arch.persistence.room.RoomDatabase.JournalMode);
+ }
+
+ public static abstract class RoomDatabase.Callback {
+ ctor public RoomDatabase.Callback();
+ method public void onCreate(android.arch.persistence.db.SupportSQLiteDatabase);
+ method public void onOpen(android.arch.persistence.db.SupportSQLiteDatabase);
+ }
+
+ public static final class RoomDatabase.JournalMode extends java.lang.Enum {
+ method public static android.arch.persistence.room.RoomDatabase.JournalMode valueOf(java.lang.String);
+ method public static final android.arch.persistence.room.RoomDatabase.JournalMode[] values();
+ enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode AUTOMATIC;
+ enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode TRUNCATE;
+ enum_constant public static final android.arch.persistence.room.RoomDatabase.JournalMode WRITE_AHEAD_LOGGING;
+ }
+
+ public static class RoomDatabase.MigrationContainer {
+ ctor public RoomDatabase.MigrationContainer();
+ method public void addMigrations(android.arch.persistence.room.migration.Migration...);
+ method public java.util.List<android.arch.persistence.room.migration.Migration> findMigrationPath(int, int);
+ }
+
+}
+
+package android.arch.persistence.room.migration {
+
+ public abstract class Migration {
+ ctor public Migration(int, int);
+ method public abstract void migrate(android.arch.persistence.db.SupportSQLiteDatabase);
+ field public final int endVersion;
+ field public final int startVersion;
+ }
+
+}
+
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index 739725c..205c030 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -30,13 +30,13 @@
}
dependencies {
- api(project(":room:common"))
- api(project(":persistence:db-framework"))
- api(project(":persistence:db"))
- api(project(":arch:runtime"))
- compileOnly project(":paging:common")
- compileOnly project(":lifecycle:runtime")
- compileOnly project(":lifecycle:extensions")
+ api(project(":room:room-common"))
+ api(project(":sqlite:sqlite-framework"))
+ api(project(":sqlite:sqlite"))
+ api(project(":arch:core-runtime"))
+ compileOnly project(":paging:paging-common")
+ compileOnly project(":lifecycle:lifecycle-runtime")
+ compileOnly project(":lifecycle:lifecycle-extensions")
api(SUPPORT_CORE_UTILS, libs.support_exclude_config)
testImplementation(project(":arch:core-testing"))
diff --git a/room/runtime/proguard-rules.pro b/room/runtime/proguard-rules.pro
index b293cd8..c148df3 100644
--- a/room/runtime/proguard-rules.pro
+++ b/room/runtime/proguard-rules.pro
@@ -1,2 +1,2 @@
--keep public class * extends android.arch.persistence.room.RoomDatabase
--dontwarn android.arch.persistence.room.paging.**
\ No newline at end of file
+-keep public class * extends androidx.room.RoomDatabase
+-dontwarn androidx.room.paging.**
\ No newline at end of file
diff --git a/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java b/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java
deleted file mode 100644
index 0eb35f6..0000000
--- a/room/runtime/src/androidTest/java/android/arch/persistence/room/migration/TableInfoTest.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * 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.migration;
-
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
-import android.arch.persistence.room.util.TableInfo;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.util.Pair;
-
-import org.junit.After;
-import org.junit.Test;
-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;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class TableInfoTest {
- private SupportSQLiteDatabase mDb;
-
- @Test
- public void readSimple() {
- mDb = createDatabase(
- "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
- + "name TEXT)");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo("foo",
- toMap(new TableInfo.Column("id", "INTEGER", false, 1),
- new TableInfo.Column("name", "TEXT", false, 0)),
- Collections.<TableInfo.ForeignKey>emptySet())));
- }
-
- @Test
- public void multiplePrimaryKeys() {
- mDb = createDatabase(
- "CREATE TABLE foo (id INTEGER,"
- + "name TEXT, PRIMARY KEY(name, id))");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo("foo",
- toMap(new TableInfo.Column("id", "INTEGER", false, 2),
- new TableInfo.Column("name", "TEXT", false, 1)),
- Collections.<TableInfo.ForeignKey>emptySet())));
- }
-
- @Test
- public void alteredTable() {
- mDb = createDatabase(
- "CREATE TABLE foo (id INTEGER,"
- + "name TEXT, PRIMARY KEY(name))");
- mDb.execSQL("ALTER TABLE foo ADD COLUMN added REAL;");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo("foo",
- toMap(new TableInfo.Column("id", "INTEGER", false, 0),
- new TableInfo.Column("name", "TEXT", false, 1),
- new TableInfo.Column("added", "REAL", false, 0)),
- Collections.<TableInfo.ForeignKey>emptySet())));
- }
-
- @Test
- public void nonNull() {
- mDb = createDatabase(
- "CREATE TABLE foo (name TEXT NOT NULL)");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo("foo",
- toMap(new TableInfo.Column("name", "TEXT", true, 0)),
- Collections.<TableInfo.ForeignKey>emptySet())));
- }
-
- @Test
- public void defaultValue() {
- mDb = createDatabase(
- "CREATE TABLE foo (name TEXT DEFAULT blah)");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo(
- "foo",
- toMap(new TableInfo.Column("name", "TEXT", false, 0)),
- Collections.<TableInfo.ForeignKey>emptySet())));
- }
-
- @Test
- public void foreignKey() {
- mDb = createDatabase(
- "CREATE TABLE foo (name TEXT)",
- "CREATE TABLE bar(barName TEXT, FOREIGN KEY(barName) REFERENCES foo(name))"
- );
- TableInfo info = TableInfo.read(mDb, "bar");
- assertThat(info.foreignKeys.size(), is(1));
- final TableInfo.ForeignKey foreignKey = info.foreignKeys.iterator().next();
- assertThat(foreignKey.columnNames, is(singletonList("barName")));
- assertThat(foreignKey.referenceColumnNames, is(singletonList("name")));
- assertThat(foreignKey.onDelete, is("NO ACTION"));
- assertThat(foreignKey.onUpdate, is("NO ACTION"));
- assertThat(foreignKey.referenceTable, is("foo"));
- }
-
- @Test
- public void multipleForeignKeys() {
- mDb = createDatabase(
- "CREATE TABLE foo (name TEXT, lastName TEXT)",
- "CREATE TABLE foo2 (name TEXT, lastName TEXT)",
- "CREATE TABLE bar(barName TEXT, barLastName TEXT, "
- + " FOREIGN KEY(barName) REFERENCES foo(name) ON UPDATE SET NULL,"
- + " FOREIGN KEY(barLastName) REFERENCES foo2(lastName) ON DELETE CASCADE)");
- TableInfo info = TableInfo.read(mDb, "bar");
- assertThat(info.foreignKeys.size(), is(2));
- Set<TableInfo.ForeignKey> expected = new HashSet<>();
- expected.add(new TableInfo.ForeignKey("foo2", // table
- "CASCADE", // on delete
- "NO ACTION", // on update
- singletonList("barLastName"), // my
- singletonList("lastName")) // ref
- );
- expected.add(new TableInfo.ForeignKey("foo", // table
- "NO ACTION", // on delete
- "SET NULL", // on update
- singletonList("barName"), // mine
- singletonList("name")/*ref*/));
- assertThat(info.foreignKeys, equalTo(expected));
- }
-
- @Test
- public void compositeForeignKey() {
- mDb = createDatabase(
- "CREATE TABLE foo (name TEXT, lastName TEXT)",
- "CREATE TABLE bar(barName TEXT, barLastName TEXT, "
- + " FOREIGN KEY(barName, barLastName) REFERENCES foo(name, lastName)"
- + " ON UPDATE cascade ON DELETE RESTRICT)");
- TableInfo info = TableInfo.read(mDb, "bar");
- assertThat(info.foreignKeys.size(), is(1));
- TableInfo.ForeignKey expected = new TableInfo.ForeignKey(
- "foo", // table
- "RESTRICT", // on delete
- "CASCADE", // on update
- asList("barName", "barLastName"), // my columns
- asList("name", "lastName") // ref columns
- );
- assertThat(info.foreignKeys.iterator().next(), is(expected));
- }
-
- @Test
- public void caseInsensitiveTypeName() {
- mDb = createDatabase(
- "CREATE TABLE foo (n integer)");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo(
- "foo",
- toMap(new TableInfo.Column("n", "INTEGER", false, 0)),
- 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"))))
- ));
- }
-
- @Test
- public void compatColumnTypes() {
- // see:https://www.sqlite.org/datatype3.html 3.1
- List<Pair<String, String>> testCases = Arrays.asList(
- new Pair<>("TINYINT", "integer"),
- new Pair<>("VARCHAR", "text"),
- new Pair<>("DOUBLE", "real"),
- new Pair<>("BOOLEAN", "numeric"),
- new Pair<>("FLOATING POINT", "integer")
- );
- for (Pair<String, String> testCase : testCases) {
- mDb = createDatabase(
- "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
- + "name " + testCase.first + ")");
- TableInfo info = TableInfo.read(mDb, "foo");
- assertThat(info, is(new TableInfo("foo",
- toMap(new TableInfo.Column("id", "INTEGER", false, 1),
- new TableInfo.Column("name", testCase.second, false, 0)),
- Collections.<TableInfo.ForeignKey>emptySet())));
- }
- }
-
- private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
- Map<String, TableInfo.Column> result = new HashMap<>();
- for (TableInfo.Column column : columns) {
- result.put(column.name, column);
- }
- 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()) {
- mDb.close();
- }
- }
-
- private static SupportSQLiteDatabase createDatabase(final String... queries) {
- return new FrameworkSQLiteOpenHelperFactory().create(
- SupportSQLiteOpenHelper.Configuration
- .builder(InstrumentationRegistry.getTargetContext())
- .name(null)
- .callback(new SupportSQLiteOpenHelper.Callback(1) {
- @Override
- public void onCreate(SupportSQLiteDatabase db) {
- for (String query : queries) {
- db.execSQL(query);
- }
- }
-
- @Override
- public void onUpgrade(SupportSQLiteDatabase db, int oldVersion,
- int newVersion) {
- throw new IllegalStateException("should not be upgrading");
- }
- }).build()
- ).getWritableDatabase();
- }
-}
diff --git a/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java b/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java
new file mode 100644
index 0000000..8b145db
--- /dev/null
+++ b/room/runtime/src/androidTest/java/androidx/room/migration/TableInfoTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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 androidx.room.migration;
+
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import androidx.room.util.TableInfo;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import org.junit.After;
+import org.junit.Test;
+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;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TableInfoTest {
+ private SupportSQLiteDatabase mDb;
+
+ @Test
+ public void readSimple() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "name TEXT)");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("id", "INTEGER", false, 1),
+ new TableInfo.Column("name", "TEXT", false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+
+ @Test
+ public void multiplePrimaryKeys() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (id INTEGER,"
+ + "name TEXT, PRIMARY KEY(name, id))");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("id", "INTEGER", false, 2),
+ new TableInfo.Column("name", "TEXT", false, 1)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+
+ @Test
+ public void alteredTable() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (id INTEGER,"
+ + "name TEXT, PRIMARY KEY(name))");
+ mDb.execSQL("ALTER TABLE foo ADD COLUMN added REAL;");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("id", "INTEGER", false, 0),
+ new TableInfo.Column("name", "TEXT", false, 1),
+ new TableInfo.Column("added", "REAL", false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+
+ @Test
+ public void nonNull() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name TEXT NOT NULL)");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("name", "TEXT", true, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+
+ @Test
+ public void defaultValue() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name TEXT DEFAULT blah)");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo(
+ "foo",
+ toMap(new TableInfo.Column("name", "TEXT", false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+
+ @Test
+ public void foreignKey() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name TEXT)",
+ "CREATE TABLE bar(barName TEXT, FOREIGN KEY(barName) REFERENCES foo(name))"
+ );
+ TableInfo info = TableInfo.read(mDb, "bar");
+ assertThat(info.foreignKeys.size(), is(1));
+ final TableInfo.ForeignKey foreignKey = info.foreignKeys.iterator().next();
+ assertThat(foreignKey.columnNames, is(singletonList("barName")));
+ assertThat(foreignKey.referenceColumnNames, is(singletonList("name")));
+ assertThat(foreignKey.onDelete, is("NO ACTION"));
+ assertThat(foreignKey.onUpdate, is("NO ACTION"));
+ assertThat(foreignKey.referenceTable, is("foo"));
+ }
+
+ @Test
+ public void multipleForeignKeys() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name TEXT, lastName TEXT)",
+ "CREATE TABLE foo2 (name TEXT, lastName TEXT)",
+ "CREATE TABLE bar(barName TEXT, barLastName TEXT, "
+ + " FOREIGN KEY(barName) REFERENCES foo(name) ON UPDATE SET NULL,"
+ + " FOREIGN KEY(barLastName) REFERENCES foo2(lastName) ON DELETE CASCADE)");
+ TableInfo info = TableInfo.read(mDb, "bar");
+ assertThat(info.foreignKeys.size(), is(2));
+ Set<TableInfo.ForeignKey> expected = new HashSet<>();
+ expected.add(new TableInfo.ForeignKey("foo2", // table
+ "CASCADE", // on delete
+ "NO ACTION", // on update
+ singletonList("barLastName"), // my
+ singletonList("lastName")) // ref
+ );
+ expected.add(new TableInfo.ForeignKey("foo", // table
+ "NO ACTION", // on delete
+ "SET NULL", // on update
+ singletonList("barName"), // mine
+ singletonList("name")/*ref*/));
+ assertThat(info.foreignKeys, equalTo(expected));
+ }
+
+ @Test
+ public void compositeForeignKey() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (name TEXT, lastName TEXT)",
+ "CREATE TABLE bar(barName TEXT, barLastName TEXT, "
+ + " FOREIGN KEY(barName, barLastName) REFERENCES foo(name, lastName)"
+ + " ON UPDATE cascade ON DELETE RESTRICT)");
+ TableInfo info = TableInfo.read(mDb, "bar");
+ assertThat(info.foreignKeys.size(), is(1));
+ TableInfo.ForeignKey expected = new TableInfo.ForeignKey(
+ "foo", // table
+ "RESTRICT", // on delete
+ "CASCADE", // on update
+ asList("barName", "barLastName"), // my columns
+ asList("name", "lastName") // ref columns
+ );
+ assertThat(info.foreignKeys.iterator().next(), is(expected));
+ }
+
+ @Test
+ public void caseInsensitiveTypeName() {
+ mDb = createDatabase(
+ "CREATE TABLE foo (n integer)");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo(
+ "foo",
+ toMap(new TableInfo.Column("n", "INTEGER", false, 0)),
+ 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"))))
+ ));
+ }
+
+ @Test
+ public void compatColumnTypes() {
+ // see:https://www.sqlite.org/datatype3.html 3.1
+ List<Pair<String, String>> testCases = Arrays.asList(
+ new Pair<>("TINYINT", "integer"),
+ new Pair<>("VARCHAR", "text"),
+ new Pair<>("DOUBLE", "real"),
+ new Pair<>("BOOLEAN", "numeric"),
+ new Pair<>("FLOATING POINT", "integer")
+ );
+ for (Pair<String, String> testCase : testCases) {
+ mDb = createDatabase(
+ "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "name " + testCase.first + ")");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("id", "INTEGER", false, 1),
+ new TableInfo.Column("name", testCase.second, false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+ }
+
+ private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
+ Map<String, TableInfo.Column> result = new HashMap<>();
+ for (TableInfo.Column column : columns) {
+ result.put(column.name, column);
+ }
+ 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()) {
+ mDb.close();
+ }
+ }
+
+ private static SupportSQLiteDatabase createDatabase(final String... queries) {
+ return new FrameworkSQLiteOpenHelperFactory().create(
+ SupportSQLiteOpenHelper.Configuration
+ .builder(InstrumentationRegistry.getTargetContext())
+ .name(null)
+ .callback(new SupportSQLiteOpenHelper.Callback(1) {
+ @Override
+ public void onCreate(SupportSQLiteDatabase db) {
+ for (String query : queries) {
+ db.execSQL(query);
+ }
+ }
+
+ @Override
+ public void onUpgrade(SupportSQLiteDatabase db, int oldVersion,
+ int newVersion) {
+ throw new IllegalStateException("should not be upgrading");
+ }
+ }).build()
+ ).getWritableDatabase();
+ }
+}
diff --git a/room/runtime/src/main/AndroidManifest.xml b/room/runtime/src/main/AndroidManifest.xml
index 54a5b36..80e6de8 100644
--- a/room/runtime/src/main/AndroidManifest.xml
+++ b/room/runtime/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.room">
+ package="androidx.room">
</manifest>
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java b/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
deleted file mode 100644
index d032bff..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.content.Context;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * Configuration class for a {@link RoomDatabase}.
- */
-@SuppressWarnings("WeakerAccess")
-public class DatabaseConfiguration {
- /**
- * The factory to use to access the database.
- */
- @NonNull
- public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
- /**
- * The context to use while connecting to the database.
- */
- @NonNull
- public final Context context;
- /**
- * The name of the database file or null if it is an in-memory database.
- */
- @Nullable
- public final String name;
-
- /**
- * Collection of available migrations.
- */
- @NonNull
- public final RoomDatabase.MigrationContainer migrationContainer;
-
- @Nullable
- public final List<RoomDatabase.Callback> callbacks;
-
- /**
- * Whether Room should throw an exception for queries run on the main thread.
- */
- public final boolean allowMainThreadQueries;
-
- /**
- * The journal mode for this database.
- */
- public final RoomDatabase.JournalMode journalMode;
-
- /**
- * If true, Room should crash if a migration is missing.
- */
- public final boolean requireMigration;
-
- /**
- * The collection of schema versions from which migrations aren't required.
- */
- private final Set<Integer> mMigrationNotRequiredFrom;
-
- /**
- * Creates a database configuration with the given values.
- *
- * @param context The application context.
- * @param name Name of the database, can be null if it is in memory.
- * @param sqliteOpenHelperFactory The open helper factory to use.
- * @param migrationContainer The migration container for migrations.
- * @param callbacks The list of callbacks for database events.
- * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
- * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
- * @param requireMigration True if Room should require a valid migration if version changes,
- * instead of recreating the tables.
- * @param migrationNotRequiredFrom The collection of schema versions from which migrations
- * aren't required.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
- @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
- @NonNull RoomDatabase.MigrationContainer migrationContainer,
- @Nullable List<RoomDatabase.Callback> callbacks,
- boolean allowMainThreadQueries,
- RoomDatabase.JournalMode journalMode,
- boolean requireMigration,
- @Nullable Set<Integer> migrationNotRequiredFrom) {
- this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
- this.context = context;
- this.name = name;
- this.migrationContainer = migrationContainer;
- this.callbacks = callbacks;
- this.allowMainThreadQueries = allowMainThreadQueries;
- this.journalMode = journalMode;
- this.requireMigration = requireMigration;
- this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
- }
-
- /**
- * Returns whether a migration is required from the specified version.
- *
- * @param version The schema version.
- * @return True if a valid migration is required, false otherwise.
- */
- public boolean isMigrationRequiredFrom(int version) {
- // Migrations are required from this version if we generally require migrations AND EITHER
- // there are no exceptions OR the supplied version is not one of the exceptions.
- return requireMigration
- && (mMigrationNotRequiredFrom == null
- || !mMigrationNotRequiredFrom.contains(version));
-
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java b/room/runtime/src/main/java/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java
deleted file mode 100644
index 373b122..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.arch.persistence.db.SupportSQLiteStatement;
-import android.support.annotation.RestrictTo;
-
-/**
- * Implementations of this class knows how to delete or update a particular entity.
- * <p>
- * This is an internal library class and all of its implementations are auto-generated.
- *
- * @param <T> The type parameter of the entity to be deleted
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@SuppressWarnings({"WeakerAccess", "unused"})
-public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
- /**
- * Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
- * database.
- *
- * @param database The database to delete / update the item in.
- */
- public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
- super(database);
- }
-
- /**
- * Create the deletion or update query
- *
- * @return An SQL query that can delete or update instances of T.
- */
- @Override
- protected abstract String createQuery();
-
- /**
- * Binds the entity into the given statement.
- *
- * @param statement The SQLite statement that prepared for the query returned from
- * createQuery.
- * @param entity The entity of type T.
- */
- protected abstract void bind(SupportSQLiteStatement statement, T entity);
-
- /**
- * Deletes or updates the given entities in the database and returns the affected row count.
- *
- * @param entity The entity to delete or update
- * @return The number of affected rows
- */
- public final int handle(T entity) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- bind(stmt, entity);
- return stmt.executeUpdateDelete();
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Deletes or updates the given entities in the database and returns the affected row count.
- *
- * @param entities Entities to delete or update
- * @return The number of affected rows
- */
- public final int handleMultiple(Iterable<T> entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- int total = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- total += stmt.executeUpdateDelete();
- }
- return total;
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Deletes or updates the given entities in the database and returns the affected row count.
- *
- * @param entities Entities to delete or update
- * @return The number of affected rows
- */
- public final int handleMultiple(T[] entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- int total = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- total += stmt.executeUpdateDelete();
- }
- return total;
- } finally {
- release(stmt);
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/EntityInsertionAdapter.java b/room/runtime/src/main/java/android/arch/persistence/room/EntityInsertionAdapter.java
deleted file mode 100644
index 6cfa332..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/EntityInsertionAdapter.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.arch.persistence.db.SupportSQLiteStatement;
-import android.support.annotation.RestrictTo;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * Implementations of this class knows how to insert a particular entity.
- * <p>
- * This is an internal library class and all of its implementations are auto-generated.
- *
- * @param <T> The type parameter of the entity to be inserted
- * @hide
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
- /**
- * Creates an InsertionAdapter that can insert the entity type T into the given database.
- *
- * @param database The database to insert into.
- */
- public EntityInsertionAdapter(RoomDatabase database) {
- super(database);
- }
-
- /**
- * Binds the entity into the given statement.
- *
- * @param statement The SQLite statement that prepared for the query returned from
- * createInsertQuery.
- * @param entity The entity of type T.
- */
- protected abstract void bind(SupportSQLiteStatement statement, T entity);
-
- /**
- * Inserts the entity into the database.
- *
- * @param entity The entity to insert
- */
- public final void insert(T entity) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- bind(stmt, entity);
- stmt.executeInsert();
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database.
- *
- * @param entities Entities to insert
- */
- public final void insert(T[] entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- for (T entity : entities) {
- bind(stmt, entity);
- stmt.executeInsert();
- }
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database.
- *
- * @param entities Entities to insert
- */
- public final void insert(Iterable<T> entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- for (T entity : entities) {
- bind(stmt, entity);
- stmt.executeInsert();
- }
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entity into the database and returns the row id.
- *
- * @param entity The entity to insert
- * @return The SQLite row id
- */
- public final long insertAndReturnId(T entity) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- bind(stmt, entity);
- return stmt.executeInsert();
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database and returns the row ids.
- *
- * @param entities Entities to insert
- * @return The SQLite row ids
- */
- public final long[] insertAndReturnIdsArray(Collection<T> entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- final long[] result = new long[entities.size()];
- int index = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- result[index] = stmt.executeInsert();
- index++;
- }
- return result;
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database and returns the row ids.
- *
- * @param entities Entities to insert
- * @return The SQLite row ids
- */
- public final long[] insertAndReturnIdsArray(T[] entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- final long[] result = new long[entities.length];
- int index = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- result[index] = stmt.executeInsert();
- index++;
- }
- return result;
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database and returns the row ids.
- *
- * @param entities Entities to insert
- * @return The SQLite row ids
- */
- public final Long[] insertAndReturnIdsArrayBox(Collection<T> entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- final Long[] result = new Long[entities.size()];
- int index = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- result[index] = stmt.executeInsert();
- index++;
- }
- return result;
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database and returns the row ids.
- *
- * @param entities Entities to insert
- * @return The SQLite row ids
- */
- public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- final Long[] result = new Long[entities.length];
- int index = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- result[index] = stmt.executeInsert();
- index++;
- }
- return result;
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database and returns the row ids.
- *
- * @param entities Entities to insert
- * @return The SQLite row ids
- */
- public final List<Long> insertAndReturnIdsList(T[] entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- final List<Long> result = new ArrayList<>(entities.length);
- int index = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- result.add(index, stmt.executeInsert());
- index++;
- }
- return result;
- } finally {
- release(stmt);
- }
- }
-
- /**
- * Inserts the given entities into the database and returns the row ids.
- *
- * @param entities Entities to insert
- * @return The SQLite row ids
- */
- public final List<Long> insertAndReturnIdsList(Collection<T> entities) {
- final SupportSQLiteStatement stmt = acquire();
- try {
- final List<Long> result = new ArrayList<>(entities.size());
- int index = 0;
- for (T entity : entities) {
- bind(stmt, entity);
- result.add(index, stmt.executeInsert());
- index++;
- }
- return result;
- } finally {
- release(stmt);
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java b/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
deleted file mode 100644
index 7bb4fb2..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
+++ /dev/null
@@ -1,691 +0,0 @@
-/*
- * 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 android.arch.core.executor.ArchTaskExecutor;
-import android.arch.core.internal.SafeIterableMap;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
-import android.support.v4.util.ArrayMap;
-import android.support.v4.util.ArraySet;
-import android.util.Log;
-
-import java.lang.ref.WeakReference;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.Lock;
-
-/**
- * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
- * these tables.
- */
-// We create an in memory table with (version, table_id) where version is an auto-increment primary
-// key and a table_id (hardcoded int from initialization).
-// ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
-// Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
-// After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
-// tables.
-// Each update on one of the observed tables triggers an insertion into this table, hence a
-// new version.
-// Unfortunately, we cannot override the previous row because sqlite uses the conflict resolution
-// of the outer query (the thing that triggered us) so we do a cleanup as we sync instead of letting
-// SQLite override the rows.
-// https://sqlite.org/lang_createtrigger.html: An ON CONFLICT clause may be specified as part of an
-// UPDATE or INSERT action within the body of the trigger. However if an ON CONFLICT clause is
-// specified as part of the statement causing the trigger to fire, then conflict handling policy of
-// the outer statement is used instead.
-public class InvalidationTracker {
-
- private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
-
- private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
-
- private static final String VERSION_COLUMN_NAME = "version";
-
- private static final String TABLE_ID_COLUMN_NAME = "table_id";
-
- private static final String CREATE_VERSION_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
- + "(" + VERSION_COLUMN_NAME
- + " INTEGER PRIMARY KEY AUTOINCREMENT, "
- + TABLE_ID_COLUMN_NAME
- + " INTEGER)";
-
- @VisibleForTesting
- static final String CLEANUP_SQL = "DELETE FROM " + UPDATE_TABLE_NAME
- + " WHERE " + VERSION_COLUMN_NAME + " NOT IN( SELECT MAX("
- + VERSION_COLUMN_NAME + ") FROM " + UPDATE_TABLE_NAME
- + " GROUP BY " + TABLE_ID_COLUMN_NAME + ")";
-
- @VisibleForTesting
- // We always clean before selecting so it is unlikely to have the same row twice and if we
- // do, it is not a big deal, just more data in the cursor.
- static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
- + " WHERE " + VERSION_COLUMN_NAME
- + " > ? ORDER BY " + VERSION_COLUMN_NAME + " ASC;";
-
- @NonNull
- @VisibleForTesting
- ArrayMap<String, Integer> mTableIdLookup;
- private String[] mTableNames;
-
- @NonNull
- @VisibleForTesting
- long[] mTableVersions;
-
- private Object[] mQueryArgs = new Object[1];
-
- // max id in the last syc
- private long mMaxVersion = 0;
-
- private final RoomDatabase mDatabase;
-
- AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
-
- private volatile boolean mInitialized = false;
-
- private volatile SupportSQLiteStatement mCleanupStatement;
-
- private ObservedTableTracker mObservedTableTracker;
-
- // should be accessed with synchronization only.
- @VisibleForTesting
- final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
-
- /**
- * Used by the generated code.
- *
- * @hide
- */
- @SuppressWarnings("WeakerAccess")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public InvalidationTracker(RoomDatabase database, String... tableNames) {
- mDatabase = database;
- mObservedTableTracker = new ObservedTableTracker(tableNames.length);
- mTableIdLookup = new ArrayMap<>();
- final int size = tableNames.length;
- mTableNames = new String[size];
- for (int id = 0; id < size; id++) {
- final String tableName = tableNames[id].toLowerCase(Locale.US);
- mTableIdLookup.put(tableName, id);
- mTableNames[id] = tableName;
- }
- mTableVersions = new long[tableNames.length];
- Arrays.fill(mTableVersions, 0);
- }
-
- /**
- * Internal method to initialize table tracking.
- * <p>
- * You should never call this method, it is called by the generated code.
- */
- void internalInit(SupportSQLiteDatabase database) {
- synchronized (this) {
- if (mInitialized) {
- Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
- return;
- }
-
- database.beginTransaction();
- try {
- database.execSQL("PRAGMA temp_store = MEMORY;");
- database.execSQL("PRAGMA recursive_triggers='ON';");
- database.execSQL(CREATE_VERSION_TABLE_SQL);
- database.setTransactionSuccessful();
- } finally {
- database.endTransaction();
- }
- syncTriggers(database);
- mCleanupStatement = database.compileStatement(CLEANUP_SQL);
- mInitialized = true;
- }
- }
-
- private static void appendTriggerName(StringBuilder builder, String tableName,
- String triggerType) {
- builder.append("`")
- .append("room_table_modification_trigger_")
- .append(tableName)
- .append("_")
- .append(triggerType)
- .append("`");
- }
-
- private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
- final String tableName = mTableNames[tableId];
- StringBuilder stringBuilder = new StringBuilder();
- for (String trigger : TRIGGERS) {
- stringBuilder.setLength(0);
- stringBuilder.append("DROP TRIGGER IF EXISTS ");
- appendTriggerName(stringBuilder, tableName, trigger);
- writableDb.execSQL(stringBuilder.toString());
- }
- }
-
- private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
- final String tableName = mTableNames[tableId];
- StringBuilder stringBuilder = new StringBuilder();
- for (String trigger : TRIGGERS) {
- stringBuilder.setLength(0);
- stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
- appendTriggerName(stringBuilder, tableName, trigger);
- stringBuilder.append(" AFTER ")
- .append(trigger)
- .append(" ON `")
- .append(tableName)
- .append("` BEGIN INSERT OR REPLACE INTO ")
- .append(UPDATE_TABLE_NAME)
- .append(" VALUES(null, ")
- .append(tableId)
- .append("); END");
- writableDb.execSQL(stringBuilder.toString());
- }
- }
-
- /**
- * Adds the given observer to the observers list and it will be notified if any table it
- * observes changes.
- * <p>
- * Database changes are pulled on another thread so in some race conditions, the observer might
- * be invoked for changes that were done before it is added.
- * <p>
- * If the observer already exists, this is a no-op call.
- * <p>
- * If one of the tables in the Observer does not exist in the database, this method throws an
- * {@link IllegalArgumentException}.
- *
- * @param observer The observer which listens the database for changes.
- */
- @WorkerThread
- public void addObserver(@NonNull Observer observer) {
- final String[] tableNames = observer.mTables;
- int[] tableIds = new int[tableNames.length];
- final int size = tableNames.length;
- long[] versions = new long[tableNames.length];
-
- // TODO sync versions ?
- for (int i = 0; i < size; i++) {
- Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
- if (tableId == null) {
- throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
- }
- tableIds[i] = tableId;
- versions[i] = mMaxVersion;
- }
- ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames, versions);
- ObserverWrapper currentObserver;
- synchronized (mObserverMap) {
- currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
- }
- if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
- syncTriggers();
- }
- }
-
- /**
- * Adds an observer but keeps a weak reference back to it.
- * <p>
- * Note that you cannot remove this observer once added. It will be automatically removed
- * when the observer is GC'ed.
- *
- * @param observer The observer to which InvalidationTracker will keep a weak reference.
- * @hide
- */
- @SuppressWarnings("unused")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public void addWeakObserver(Observer observer) {
- addObserver(new WeakObserver(this, observer));
- }
-
- /**
- * Removes the observer from the observers list.
- *
- * @param observer The observer to remove.
- */
- @SuppressWarnings("WeakerAccess")
- @WorkerThread
- public void removeObserver(@NonNull final Observer observer) {
- ObserverWrapper wrapper;
- synchronized (mObserverMap) {
- wrapper = mObserverMap.remove(observer);
- }
- if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
- syncTriggers();
- }
- }
-
- private boolean ensureInitialization() {
- if (!mDatabase.isOpen()) {
- return false;
- }
- if (!mInitialized) {
- // trigger initialization
- mDatabase.getOpenHelper().getWritableDatabase();
- }
- if (!mInitialized) {
- Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
- return false;
- }
- return true;
- }
-
- @VisibleForTesting
- Runnable mRefreshRunnable = new Runnable() {
- @Override
- public void run() {
- final Lock closeLock = mDatabase.getCloseLock();
- boolean hasUpdatedTable = false;
- try {
- closeLock.lock();
-
- if (!ensureInitialization()) {
- return;
- }
-
- 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;
- if (mDatabase.mWriteAheadLoggingEnabled) {
- // This transaction has to be on the underlying DB rather than the RoomDatabase
- // in order to avoid a recursive loop after endTransaction.
- SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
- try {
- db.beginTransaction();
- hasUpdatedTable = checkUpdatedTable();
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- } else {
- hasUpdatedTable = checkUpdatedTable();
- }
- } catch (IllegalStateException | SQLiteException exception) {
- // may happen if db is closed. just log.
- Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
- exception);
- } finally {
- closeLock.unlock();
- }
- if (hasUpdatedTable) {
- synchronized (mObserverMap) {
- for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
- entry.getValue().checkForInvalidation(mTableVersions);
- }
- }
- }
- }
-
- private boolean checkUpdatedTable() {
- boolean hasUpdatedTable = false;
- Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs);
- //noinspection TryFinallyCanBeTryWithResources
- try {
- while (cursor.moveToNext()) {
- final long version = cursor.getLong(0);
- final int tableId = cursor.getInt(1);
-
- mTableVersions[tableId] = version;
- hasUpdatedTable = true;
- // result is ordered so we can safely do this assignment
- mMaxVersion = version;
- }
- } finally {
- cursor.close();
- }
- return hasUpdatedTable;
- }
- };
-
- /**
- * Enqueues a task to refresh the list of updated tables.
- * <p>
- * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
- * if you have another connection to the database or directly use {@link
- * SupportSQLiteDatabase}, you may need to call this manually.
- */
- @SuppressWarnings("WeakerAccess")
- public void refreshVersionsAsync() {
- // TODO we should consider doing this sync instead of async.
- if (mPendingRefresh.compareAndSet(false, true)) {
- ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
- }
- }
-
- /**
- * Check versions for tables, and run observers synchronously if tables have been updated.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @WorkerThread
- public void refreshVersionsSync() {
- syncTriggers();
- mRefreshRunnable.run();
- }
-
- void syncTriggers(SupportSQLiteDatabase database) {
- if (database.inTransaction()) {
- // we won't run this inside another transaction.
- return;
- }
- try {
- // This method runs in a while loop because while changes are synced to db, another
- // runnable may be skipped. If we cause it to skip, we need to do its work.
- while (true) {
- Lock closeLock = mDatabase.getCloseLock();
- closeLock.lock();
- try {
- // there is a potential race condition where another mSyncTriggers runnable
- // can start running right after we get the tables list to sync.
- final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
- if (tablesToSync == null) {
- return;
- }
- final int limit = tablesToSync.length;
- try {
- database.beginTransaction();
- for (int tableId = 0; tableId < limit; tableId++) {
- switch (tablesToSync[tableId]) {
- case ObservedTableTracker.ADD:
- startTrackingTable(database, tableId);
- break;
- case ObservedTableTracker.REMOVE:
- stopTrackingTable(database, tableId);
- break;
- }
- }
- database.setTransactionSuccessful();
- } finally {
- database.endTransaction();
- }
- mObservedTableTracker.onSyncCompleted();
- } finally {
- closeLock.unlock();
- }
- }
- } catch (IllegalStateException | SQLiteException exception) {
- // may happen if db is closed. just log.
- Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
- exception);
- }
- }
-
- /**
- * Called by RoomDatabase before each beginTransaction call.
- * <p>
- * It is important that pending trigger changes are applied to the database before any query
- * runs. Otherwise, we may miss some changes.
- * <p>
- * This api should eventually be public.
- */
- void syncTriggers() {
- if (!mDatabase.isOpen()) {
- return;
- }
- syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
- }
-
- /**
- * Wraps an observer and keeps the table information.
- * <p>
- * Internally table ids are used which may change from database to database so the table
- * related information is kept here rather than in the Observer.
- */
- @SuppressWarnings("WeakerAccess")
- static class ObserverWrapper {
- final int[] mTableIds;
- private final String[] mTableNames;
- private final long[] mVersions;
- final Observer mObserver;
- private final Set<String> mSingleTableSet;
-
- ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames, long[] versions) {
- mObserver = observer;
- mTableIds = tableIds;
- mTableNames = tableNames;
- mVersions = versions;
- if (tableIds.length == 1) {
- ArraySet<String> set = new ArraySet<>();
- set.add(mTableNames[0]);
- mSingleTableSet = Collections.unmodifiableSet(set);
- } else {
- mSingleTableSet = null;
- }
- }
-
- void checkForInvalidation(long[] versions) {
- Set<String> invalidatedTables = null;
- final int size = mTableIds.length;
- for (int index = 0; index < size; index++) {
- final int tableId = mTableIds[index];
- final long newVersion = versions[tableId];
- final long currentVersion = mVersions[index];
- if (currentVersion < newVersion) {
- mVersions[index] = newVersion;
- if (size == 1) {
- // Optimization for a single-table observer
- invalidatedTables = mSingleTableSet;
- } else {
- if (invalidatedTables == null) {
- invalidatedTables = new ArraySet<>(size);
- }
- invalidatedTables.add(mTableNames[index]);
- }
- }
- }
- if (invalidatedTables != null) {
- mObserver.onInvalidated(invalidatedTables);
- }
- }
- }
-
- /**
- * An observer that can listen for changes in the database.
- */
- public abstract static class Observer {
- final String[] mTables;
-
- /**
- * Observes the given list of tables.
- *
- * @param firstTable The table name
- * @param rest More table names
- */
- @SuppressWarnings("unused")
- protected Observer(@NonNull String firstTable, String... rest) {
- mTables = Arrays.copyOf(rest, rest.length + 1);
- mTables[rest.length] = firstTable;
- }
-
- /**
- * Observes the given list of tables.
- *
- * @param tables The list of tables to observe for changes.
- */
- public Observer(@NonNull String[] tables) {
- // copy tables in case user modifies them afterwards
- mTables = Arrays.copyOf(tables, tables.length);
- }
-
- /**
- * Called when one of the observed tables is invalidated in the database.
- *
- * @param tables A set of invalidated tables. This is useful when the observer targets
- * multiple tables and want to know which table is invalidated.
- */
- public abstract void onInvalidated(@NonNull Set<String> tables);
- }
-
-
- /**
- * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
- * triggers in the database.
- * <p>
- * This class is thread safe
- */
- static class ObservedTableTracker {
- static final int NO_OP = 0; // don't change trigger state for this table
- static final int ADD = 1; // add triggers for this table
- static final int REMOVE = 2; // remove triggers for this table
-
- // number of observers per table
- final long[] mTableObservers;
- // trigger state for each table at last sync
- // this field is updated when syncAndGet is called.
- final boolean[] mTriggerStates;
- // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
- final int[] mTriggerStateChanges;
-
- boolean mNeedsSync;
-
- /**
- * After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
- * returning any non-null value from getTablesToSync.
- * This allows us to workaround any multi-threaded state syncing issues.
- */
- boolean mPendingSync;
-
- ObservedTableTracker(int tableCount) {
- mTableObservers = new long[tableCount];
- mTriggerStates = new boolean[tableCount];
- mTriggerStateChanges = new int[tableCount];
- Arrays.fill(mTableObservers, 0);
- Arrays.fill(mTriggerStates, false);
- }
-
- /**
- * @return true if # of triggers is affected.
- */
- boolean onAdded(int... tableIds) {
- boolean needTriggerSync = false;
- synchronized (this) {
- for (int tableId : tableIds) {
- final long prevObserverCount = mTableObservers[tableId];
- mTableObservers[tableId] = prevObserverCount + 1;
- if (prevObserverCount == 0) {
- mNeedsSync = true;
- needTriggerSync = true;
- }
- }
- }
- return needTriggerSync;
- }
-
- /**
- * @return true if # of triggers is affected.
- */
- boolean onRemoved(int... tableIds) {
- boolean needTriggerSync = false;
- synchronized (this) {
- for (int tableId : tableIds) {
- final long prevObserverCount = mTableObservers[tableId];
- mTableObservers[tableId] = prevObserverCount - 1;
- if (prevObserverCount == 1) {
- mNeedsSync = true;
- needTriggerSync = true;
- }
- }
- }
- return needTriggerSync;
- }
-
- /**
- * If this returns non-null, you must call onSyncCompleted.
- *
- * @return int[] An int array where the index for each tableId has the action for that
- * table.
- */
- @Nullable
- int[] getTablesToSync() {
- synchronized (this) {
- if (!mNeedsSync || mPendingSync) {
- return null;
- }
- final int tableCount = mTableObservers.length;
- for (int i = 0; i < tableCount; i++) {
- final boolean newState = mTableObservers[i] > 0;
- if (newState != mTriggerStates[i]) {
- mTriggerStateChanges[i] = newState ? ADD : REMOVE;
- } else {
- mTriggerStateChanges[i] = NO_OP;
- }
- mTriggerStates[i] = newState;
- }
- mPendingSync = true;
- mNeedsSync = false;
- return mTriggerStateChanges;
- }
- }
-
- /**
- * if getTablesToSync returned non-null, the called should call onSyncCompleted once it
- * is done.
- */
- void onSyncCompleted() {
- synchronized (this) {
- mPendingSync = false;
- }
- }
- }
-
- /**
- * An Observer wrapper that keeps a weak reference to the given object.
- * <p>
- * This class with automatically unsubscribe when the wrapped observer goes out of memory.
- */
- static class WeakObserver extends Observer {
- final InvalidationTracker mTracker;
- final WeakReference<Observer> mDelegateRef;
-
- WeakObserver(InvalidationTracker tracker, Observer delegate) {
- super(delegate.mTables);
- mTracker = tracker;
- mDelegateRef = new WeakReference<>(delegate);
- }
-
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- final Observer observer = mDelegateRef.get();
- if (observer == null) {
- mTracker.removeObserver(this);
- } else {
- observer.onInvalidated(tables);
- }
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/Room.java b/room/runtime/src/main/java/android/arch/persistence/room/Room.java
deleted file mode 100644
index 9b168fc..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/Room.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.content.Context;
-import android.support.annotation.NonNull;
-
-/**
- * Utility class for Room.
- */
-@SuppressWarnings("unused")
-public class Room {
- static final String LOG_TAG = "ROOM";
- /**
- * The master table where room keeps its metadata information.
- */
- public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
- private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
-
- /**
- * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
- * should keep a reference to it and re-use it.
- *
- * @param context The context for the database. This is usually the Application context.
- * @param klass The abstract class which is annotated with {@link Database} and extends
- * {@link RoomDatabase}.
- * @param name The name of the database file.
- * @param <T> The type of the database class.
- * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
- */
- @SuppressWarnings("WeakerAccess")
- @NonNull
- public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder(
- @NonNull Context context, @NonNull Class<T> klass, @NonNull String name) {
- //noinspection ConstantConditions
- if (name == null || name.trim().length() == 0) {
- throw new IllegalArgumentException("Cannot build a database with null or empty name."
- + " If you are trying to create an in memory database, use Room"
- + ".inMemoryDatabaseBuilder");
- }
- return new RoomDatabase.Builder<>(context, klass, name);
- }
-
- /**
- * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
- * database disappears when the process is killed.
- * Once a database is built, you should keep a reference to it and re-use it.
- *
- * @param context The context for the database. This is usually the Application context.
- * @param klass The abstract class which is annotated with {@link Database} and extends
- * {@link RoomDatabase}.
- * @param <T> The type of the database class.
- * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
- */
- @NonNull
- public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder(
- @NonNull Context context, @NonNull Class<T> klass) {
- return new RoomDatabase.Builder<>(context, klass, null);
- }
-
- @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
- @NonNull
- static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {
- final String fullPackage = klass.getPackage().getName();
- String name = klass.getCanonicalName();
- final String postPackageName = fullPackage.isEmpty()
- ? name
- : (name.substring(fullPackage.length() + 1));
- final String implName = postPackageName.replace('.', '_') + suffix;
- //noinspection TryWithIdenticalCatches
- try {
-
- @SuppressWarnings("unchecked")
- final Class<T> aClass = (Class<T>) Class.forName(
- fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
- return aClass.newInstance();
- } catch (ClassNotFoundException e) {
- throw new RuntimeException("cannot find implementation for "
- + klass.getCanonicalName() + ". " + implName + " does not exist");
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot access the constructor"
- + klass.getCanonicalName());
- } catch (InstantiationException e) {
- throw new RuntimeException("Failed to create an instance of "
- + klass.getCanonicalName());
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
deleted file mode 100644
index 90131ed..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
+++ /dev/null
@@ -1,749 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.persistence.db.SimpleSQLiteQuery;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
-import android.arch.persistence.room.migration.Migration;
-import android.content.Context;
-import android.database.Cursor;
-import android.os.Build;
-import android.support.annotation.CallSuper;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.WorkerThread;
-import android.support.v4.app.ActivityManagerCompat;
-import android.support.v4.util.SparseArrayCompat;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Base class for all Room databases. All classes that are annotated with {@link Database} must
- * extend this class.
- * <p>
- * RoomDatabase provides direct access to the underlying database implementation but you should
- * prefer using {@link Dao} classes.
- *
- * @see Database
- */
-//@SuppressWarnings({"unused", "WeakerAccess"})
-public abstract class RoomDatabase {
- private static final String DB_IMPL_SUFFIX = "_Impl";
- /**
- * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static final int MAX_BIND_PARAMETER_CNT = 999;
- // set by the generated open helper.
- protected volatile SupportSQLiteDatabase mDatabase;
- private SupportSQLiteOpenHelper mOpenHelper;
- private final InvalidationTracker mInvalidationTracker;
- private boolean mAllowMainThreadQueries;
- boolean mWriteAheadLoggingEnabled;
-
- @Nullable
- protected List<Callback> mCallbacks;
-
- private final ReentrantLock mCloseLock = new ReentrantLock();
-
- /**
- * {@link InvalidationTracker} uses this lock to prevent the database from closing while it is
- * querying database updates.
- *
- * @return The lock for {@link #close()}.
- */
- Lock getCloseLock() {
- return mCloseLock;
- }
-
- /**
- * Creates a RoomDatabase.
- * <p>
- * You cannot create an instance of a database, instead, you should acquire it via
- * {@link Room#databaseBuilder(Context, Class, String)} or
- * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
- */
- public RoomDatabase() {
- mInvalidationTracker = createInvalidationTracker();
- }
-
- /**
- * Called by {@link Room} when it is initialized.
- *
- * @param configuration The database configuration.
- */
- @CallSuper
- public void init(@NonNull DatabaseConfiguration configuration) {
- mOpenHelper = createOpenHelper(configuration);
- boolean wal = false;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING;
- mOpenHelper.setWriteAheadLoggingEnabled(wal);
- }
- mCallbacks = configuration.callbacks;
- mAllowMainThreadQueries = configuration.allowMainThreadQueries;
- mWriteAheadLoggingEnabled = wal;
- }
-
- /**
- * Returns the SQLite open helper used by this database.
- *
- * @return The SQLite open helper used by this database.
- */
- @NonNull
- public SupportSQLiteOpenHelper getOpenHelper() {
- return mOpenHelper;
- }
-
- /**
- * Creates the open helper to access the database. Generated class already implements this
- * method.
- * Note that this method is called when the RoomDatabase is initialized.
- *
- * @param config The configuration of the Room database.
- * @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
- */
- @NonNull
- protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
-
- /**
- * Called when the RoomDatabase is created.
- * <p>
- * This is already implemented by the generated code.
- *
- * @return Creates a new InvalidationTracker.
- */
- @NonNull
- protected abstract InvalidationTracker createInvalidationTracker();
-
- /**
- * Deletes all rows from all the tables that are registered to this database as
- * {@link Database#entities()}.
- * <p>
- * This does NOT reset the auto-increment value generated by {@link PrimaryKey#autoGenerate()}.
- */
- @WorkerThread
- public abstract void clearAllTables();
-
- /**
- * Returns true if database connection is open and initialized.
- *
- * @return true if the database connection is open, false otherwise.
- */
- public boolean isOpen() {
- final SupportSQLiteDatabase db = mDatabase;
- return db != null && db.isOpen();
- }
-
- /**
- * Closes the database if it is already open.
- */
- public void close() {
- if (isOpen()) {
- try {
- mCloseLock.lock();
- mOpenHelper.close();
- } finally {
- mCloseLock.unlock();
- }
- }
- }
-
- /**
- * Asserts that we are not on the main thread.
- *
- * @hide
- */
- @SuppressWarnings("WeakerAccess")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- // used in generated code
- public void assertNotMainThread() {
- if (mAllowMainThreadQueries) {
- return;
- }
- 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.");
- }
- }
-
- // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
- // methods we are using and also helps unit tests to mock this class without mocking
- // all SQLite database methods.
-
- /**
- * Convenience method to query the database with arguments.
- *
- * @param query The sql query
- * @param args The bind arguments for the placeholders in the query
- *
- * @return A Cursor obtained by running the given query in the Room database.
- */
- public Cursor query(String query, @Nullable Object[] args) {
- return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
- }
-
- /**
- * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
- *
- * @param query The Query which includes the SQL and a bind callback for bind arguments.
- * @return Result of the query.
- */
- public Cursor query(SupportSQLiteQuery query) {
- assertNotMainThread();
- return mOpenHelper.getWritableDatabase().query(query);
- }
-
- /**
- * Wrapper for {@link SupportSQLiteDatabase#compileStatement(String)}.
- *
- * @param sql The query to compile.
- * @return The compiled query.
- */
- public SupportSQLiteStatement compileStatement(@NonNull String sql) {
- assertNotMainThread();
- return mOpenHelper.getWritableDatabase().compileStatement(sql);
- }
-
- /**
- * Wrapper for {@link SupportSQLiteDatabase#beginTransaction()}.
- */
- public void beginTransaction() {
- assertNotMainThread();
- SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
- mInvalidationTracker.syncTriggers(database);
- database.beginTransaction();
- }
-
- /**
- * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}.
- */
- public void endTransaction() {
- mOpenHelper.getWritableDatabase().endTransaction();
- if (!inTransaction()) {
- // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last
- // endTransaction call to do it.
- mInvalidationTracker.refreshVersionsAsync();
- }
- }
-
- /**
- * Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
- */
- public void setTransactionSuccessful() {
- mOpenHelper.getWritableDatabase().setTransactionSuccessful();
- }
-
- /**
- * Executes the specified {@link Runnable} in a database transaction. The transaction will be
- * marked as successful unless an exception is thrown in the {@link Runnable}.
- *
- * @param body The piece of code to execute.
- */
- public void runInTransaction(@NonNull Runnable body) {
- beginTransaction();
- try {
- body.run();
- setTransactionSuccessful();
- } finally {
- endTransaction();
- }
- }
-
- /**
- * Executes the specified {@link Callable} in a database transaction. The transaction will be
- * marked as successful unless an exception is thrown in the {@link Callable}.
- *
- * @param body The piece of code to execute.
- * @param <V> The type of the return value.
- * @return The value returned from the {@link Callable}.
- */
- public <V> V runInTransaction(@NonNull Callable<V> body) {
- beginTransaction();
- try {
- V result = body.call();
- setTransactionSuccessful();
- return result;
- } catch (RuntimeException e) {
- throw e;
- } catch (Exception e) {
- throw new RuntimeException("Exception in transaction", e);
- } finally {
- endTransaction();
- }
- }
-
- /**
- * Called by the generated code when database is open.
- * <p>
- * You should never call this method manually.
- *
- * @param db The database instance.
- */
- protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
- mInvalidationTracker.internalInit(db);
- }
-
- /**
- * Returns the invalidation tracker for this database.
- * <p>
- * You can use the invalidation tracker to get notified when certain tables in the database
- * are modified.
- *
- * @return The invalidation tracker for the database.
- */
- @NonNull
- public InvalidationTracker getInvalidationTracker() {
- return mInvalidationTracker;
- }
-
- /**
- * Returns true if current thread is in a transaction.
- *
- * @return True if there is an active transaction in current thread, false otherwise.
- * @see SupportSQLiteDatabase#inTransaction()
- */
- @SuppressWarnings("WeakerAccess")
- public boolean inTransaction() {
- return mOpenHelper.getWritableDatabase().inTransaction();
- }
-
- /**
- * Journal modes for SQLite database.
- *
- * @see RoomDatabase.Builder#setJournalMode(JournalMode)
- */
- public enum JournalMode {
-
- /**
- * Let Room choose the journal mode. This is the default value when no explicit value is
- * specified.
- * <p>
- * The actual value will be {@link #TRUNCATE} when the device runs API Level lower than 16
- * or it is a low-RAM device. Otherwise, {@link #WRITE_AHEAD_LOGGING} will be used.
- */
- AUTOMATIC,
-
- /**
- * Truncate journal mode.
- */
- TRUNCATE,
-
- /**
- * Write-Ahead Logging mode.
- */
- @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
- WRITE_AHEAD_LOGGING;
-
- /**
- * Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or
- * {@link #WRITE_AHEAD_LOGGING}.
- */
- @SuppressLint("NewApi")
- JournalMode resolve(Context context) {
- if (this != AUTOMATIC) {
- return this;
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- ActivityManager manager = (ActivityManager)
- context.getSystemService(Context.ACTIVITY_SERVICE);
- if (manager != null && !ActivityManagerCompat.isLowRamDevice(manager)) {
- return WRITE_AHEAD_LOGGING;
- }
- }
- return TRUNCATE;
- }
- }
-
- /**
- * Builder for RoomDatabase.
- *
- * @param <T> The type of the abstract database class.
- */
- public static class Builder<T extends RoomDatabase> {
- private final Class<T> mDatabaseClass;
- private final String mName;
- private final Context mContext;
- private ArrayList<Callback> mCallbacks;
-
- private SupportSQLiteOpenHelper.Factory mFactory;
- private boolean mAllowMainThreadQueries;
- private JournalMode mJournalMode;
- private boolean mRequireMigration;
- /**
- * Migrations, mapped by from-to pairs.
- */
- private final MigrationContainer mMigrationContainer;
- private Set<Integer> mMigrationsNotRequiredFrom;
- /**
- * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
- * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
- * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(Integer...)}.
- */
- private Set<Integer> mMigrationStartAndEndVersions;
-
- Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
- mContext = context;
- mDatabaseClass = klass;
- mName = name;
- mJournalMode = JournalMode.AUTOMATIC;
- mRequireMigration = true;
- mMigrationContainer = new MigrationContainer();
- }
-
- /**
- * Sets the database factory. If not set, it defaults to
- * {@link FrameworkSQLiteOpenHelperFactory}.
- *
- * @param factory The factory to use to access the database.
- * @return this
- */
- @NonNull
- public Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) {
- mFactory = factory;
- return this;
- }
-
- /**
- * Adds a migration to the builder.
- * <p>
- * Each Migration has a start and end versions and Room runs these migrations to bring the
- * database to the latest version.
- * <p>
- * If a migration item is missing between current version and the latest version, Room
- * will clear the database and recreate so even if you have no changes between 2 versions,
- * you should still provide a Migration object to the builder.
- * <p>
- * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
- * going version 3 to 5 without going to version 4). If Room opens a database at version
- * 3 and latest version is >= 5, Room will use the migration object that can migrate from
- * 3 to 5 instead of 3 to 4 and 4 to 5.
- *
- * @param migrations The migration object that can modify the database and to the necessary
- * changes.
- * @return this
- */
- @NonNull
- public Builder<T> addMigrations(@NonNull Migration... migrations) {
- if (mMigrationStartAndEndVersions == null) {
- mMigrationStartAndEndVersions = new HashSet<>();
- }
- for (Migration migration: migrations) {
- mMigrationStartAndEndVersions.add(migration.startVersion);
- mMigrationStartAndEndVersions.add(migration.endVersion);
- }
-
- mMigrationContainer.addMigrations(migrations);
- return this;
- }
-
- /**
- * Disables the main thread query check for Room.
- * <p>
- * Room ensures that Database is never accessed on the main thread because it may lock the
- * main thread and trigger an ANR. If you need to access the database from the main thread,
- * you should always use async alternatives or manually move the call to a background
- * thread.
- * <p>
- * You may want to turn this check off for testing.
- *
- * @return this
- */
- @NonNull
- public Builder<T> allowMainThreadQueries() {
- mAllowMainThreadQueries = true;
- return this;
- }
-
- /**
- * Sets the journal mode for this database.
- *
- * <p>
- * This value is ignored if the builder is initialized with
- * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
- * <p>
- * The journal mode should be consistent across multiple instances of
- * {@link RoomDatabase} for a single SQLite database file.
- * <p>
- * The default value is {@link JournalMode#AUTOMATIC}.
- *
- * @param journalMode The journal mode.
- * @return this
- */
- @NonNull
- public Builder<T> setJournalMode(@NonNull JournalMode journalMode) {
- mJournalMode = journalMode;
- return this;
- }
-
- /**
- * 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>
- * If it cannot find the set of {@link Migration}s that will bring the database to the
- * current version, it will throw an {@link IllegalStateException}.
- * <p>
- * You can call this method to change this behavior to re-create the database instead of
- * crashing.
- * <p>
- * Note that this will delete all of the data in the database tables managed by Room.
- *
- * @return this
- */
- @NonNull
- public Builder<T> fallbackToDestructiveMigration() {
- mRequireMigration = false;
- return this;
- }
-
- /**
- * Informs Room that it is allowed to destructively recreate database tables from specific
- * starting schema versions.
- * <p>
- * This functionality is the same as that provided by
- * {@link #fallbackToDestructiveMigration()}, except that this method allows the
- * specification of a set of schema versions for which destructive recreation is allowed.
- * <p>
- * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
- * to allow destructive migrations from some schema versions while still taking advantage
- * of exceptions being thrown due to unintentionally missing migrations.
- * <p>
- * Note: No versions passed to this method may also exist as either starting or ending
- * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
- * version passed to this method is found as a starting or ending version in a Migration, an
- * exception will be thrown.
- *
- * @param startVersions The set of schema versions from which Room should use a destructive
- * migration.
- * @return this
- */
- @NonNull
- public Builder<T> fallbackToDestructiveMigrationFrom(Integer... startVersions) {
- if (mMigrationsNotRequiredFrom == null) {
- mMigrationsNotRequiredFrom = new HashSet<>();
- }
- Collections.addAll(mMigrationsNotRequiredFrom, startVersions);
- return this;
- }
-
- /**
- * Adds a {@link Callback} to this database.
- *
- * @param callback The callback.
- * @return this
- */
- @NonNull
- public Builder<T> addCallback(@NonNull Callback callback) {
- if (mCallbacks == null) {
- mCallbacks = new ArrayList<>();
- }
- mCallbacks.add(callback);
- return this;
- }
-
- /**
- * Creates the databases and initializes it.
- * <p>
- * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
- * triggers.
- *
- * @return A new database instance.
- */
- @NonNull
- public T build() {
- //noinspection ConstantConditions
- if (mContext == null) {
- throw new IllegalArgumentException("Cannot provide null context for the database.");
- }
- //noinspection ConstantConditions
- if (mDatabaseClass == null) {
- throw new IllegalArgumentException("Must provide an abstract class that"
- + " extends RoomDatabase");
- }
-
- if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
- for (Integer version : mMigrationStartAndEndVersions) {
- if (mMigrationsNotRequiredFrom.contains(version)) {
- throw new IllegalArgumentException(
- "Inconsistency detected. A Migration was supplied to "
- + "addMigration(Migration... migrations) that has a start "
- + "or end version equal to a start version supplied to "
- + "fallbackToDestructiveMigrationFrom(Integer ... "
- + "startVersions). Start version: "
- + version);
- }
- }
- }
-
- if (mFactory == null) {
- mFactory = new FrameworkSQLiteOpenHelperFactory();
- }
- DatabaseConfiguration configuration =
- new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
- mCallbacks, mAllowMainThreadQueries,
- mJournalMode.resolve(mContext),
- mRequireMigration, mMigrationsNotRequiredFrom);
- T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
- db.init(configuration);
- return db;
- }
- }
-
- /**
- * A container to hold migrations. It also allows querying its contents to find migrations
- * between two versions.
- */
- public static class MigrationContainer {
- private SparseArrayCompat<SparseArrayCompat<Migration>> mMigrations =
- new SparseArrayCompat<>();
-
- /**
- * Adds the given migrations to the list of available migrations. If 2 migrations have the
- * same start-end versions, the latter migration overrides the previous one.
- *
- * @param migrations List of available migrations.
- */
- public void addMigrations(@NonNull Migration... migrations) {
- for (Migration migration : migrations) {
- addMigration(migration);
- }
- }
-
- private void addMigration(Migration migration) {
- final int start = migration.startVersion;
- final int end = migration.endVersion;
- SparseArrayCompat<Migration> targetMap = mMigrations.get(start);
- if (targetMap == null) {
- targetMap = new SparseArrayCompat<>();
- mMigrations.put(start, targetMap);
- }
- Migration existing = targetMap.get(end);
- if (existing != null) {
- Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
- }
- targetMap.append(end, migration);
- }
-
- /**
- * Finds the list of migrations that should be run to move from {@code start} version to
- * {@code end} version.
- *
- * @param start The current database version
- * @param end The target database version
- * @return An ordered list of {@link Migration} objects that should be run to migrate
- * between the given versions. If a migration path cannot be found, returns {@code null}.
- */
- @SuppressWarnings("WeakerAccess")
- @Nullable
- public List<Migration> findMigrationPath(int start, int end) {
- if (start == end) {
- return Collections.emptyList();
- }
- boolean migrateUp = end > start;
- List<Migration> result = new ArrayList<>();
- return findUpMigrationPath(result, migrateUp, start, end);
- }
-
- private List<Migration> findUpMigrationPath(List<Migration> result, boolean upgrade,
- int start, int end) {
- final int searchDirection = upgrade ? -1 : 1;
- while (upgrade ? start < end : start > end) {
- SparseArrayCompat<Migration> targetNodes = mMigrations.get(start);
- if (targetNodes == null) {
- return null;
- }
- // keys are ordered so we can start searching from one end of them.
- final int size = targetNodes.size();
- final int firstIndex;
- final int lastIndex;
-
- if (upgrade) {
- firstIndex = size - 1;
- lastIndex = -1;
- } else {
- firstIndex = 0;
- lastIndex = size;
- }
- boolean found = false;
- for (int i = firstIndex; i != lastIndex; i += searchDirection) {
- final int targetVersion = targetNodes.keyAt(i);
- final boolean shouldAddToPath;
- if (upgrade) {
- shouldAddToPath = targetVersion <= end && targetVersion > start;
- } else {
- shouldAddToPath = targetVersion >= end && targetVersion < start;
- }
- if (shouldAddToPath) {
- result.add(targetNodes.valueAt(i));
- start = targetVersion;
- found = true;
- break;
- }
- }
- if (!found) {
- return null;
- }
- }
- return result;
- }
- }
-
- /**
- * Callback for {@link RoomDatabase}.
- */
- public abstract static class Callback {
-
- /**
- * Called when the database is created for the first time. This is called after all the
- * tables are created.
- *
- * @param db The database.
- */
- public void onCreate(@NonNull SupportSQLiteDatabase db) {
- }
-
- /**
- * Called when the database has been opened.
- *
- * @param db The database.
- */
- public void onOpen(@NonNull SupportSQLiteDatabase db) {
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
deleted file mode 100644
index a921b9e..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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 android.arch.persistence.db.SimpleSQLiteQuery;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.room.migration.Migration;
-import android.database.Cursor;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.List;
-
-/**
- * An open helper that holds a reference to the configuration until the database is opened.
- *
- * @hide
- */
-@SuppressWarnings("unused")
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
- @Nullable
- private DatabaseConfiguration mConfiguration;
- @NonNull
- private final Delegate mDelegate;
- @NonNull
- private final String mIdentityHash;
- /**
- * Room v1 had a bug where the hash was not consistent if fields are reordered.
- * The new has fixes it but we still need to accept the legacy hash.
- */
- @NonNull // b/64290754
- private final String mLegacyHash;
-
- public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
- @NonNull String identityHash, @NonNull String legacyHash) {
- super(delegate.version);
- mConfiguration = configuration;
- mDelegate = delegate;
- mIdentityHash = identityHash;
- mLegacyHash = legacyHash;
- }
-
- @Override
- public void onConfigure(SupportSQLiteDatabase db) {
- super.onConfigure(db);
- }
-
- @Override
- public void onCreate(SupportSQLiteDatabase db) {
- updateIdentity(db);
- mDelegate.createAllTables(db);
- mDelegate.onCreate(db);
- }
-
- @Override
- public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
- boolean migrated = false;
- if (mConfiguration != null) {
- List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
- oldVersion, newVersion);
- if (migrations != null) {
- for (Migration migration : migrations) {
- migration.migrate(db);
- }
- mDelegate.validateMigration(db);
- updateIdentity(db);
- migrated = true;
- }
- }
- if (!migrated) {
- if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) {
- mDelegate.dropAllTables(db);
- mDelegate.createAllTables(db);
- } else {
- throw new IllegalStateException("A migration from " + oldVersion + " to "
- + newVersion + " was required but not found. Please provide the "
- + "necessary Migration path via "
- + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
- + "destructive migrations via one of the "
- + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
- }
- }
- }
-
- @Override
- public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
- onUpgrade(db, oldVersion, newVersion);
- }
-
- @Override
- public void onOpen(SupportSQLiteDatabase db) {
- super.onOpen(db);
- checkIdentity(db);
- mDelegate.onOpen(db);
- // there might be too many configurations etc, just clear it.
- mConfiguration = null;
- }
-
- private void checkIdentity(SupportSQLiteDatabase db) {
- String identityHash = "";
- if (hasRoomMasterTable(db)) {
- Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
- //noinspection TryFinallyCanBeTryWithResources
- try {
- if (cursor.moveToFirst()) {
- identityHash = cursor.getString(0);
- }
- } finally {
- cursor.close();
- }
- }
- if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
- throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
- + " you've changed schema but forgot to update the version number. You can"
- + " simply fix this by increasing the version number.");
- }
- }
-
- private void updateIdentity(SupportSQLiteDatabase db) {
- createMasterTableIfNotExists(db);
- db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
- }
-
- private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
- db.execSQL(RoomMasterTable.CREATE_QUERY);
- }
-
- private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
- Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
- + RoomMasterTable.TABLE_NAME + "'");
- //noinspection TryFinallyCanBeTryWithResources
- try {
- return cursor.moveToFirst() && cursor.getInt(0) != 0;
- } finally {
- cursor.close();
- }
- }
-
- /**
- * @hide
- */
- @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);
-
- protected abstract void onOpen(SupportSQLiteDatabase database);
-
- protected abstract void onCreate(SupportSQLiteDatabase database);
-
- /**
- * Called after a migration run to validate database integrity.
- *
- * @param db The SQLite database.
- */
- protected abstract void validateMigration(SupportSQLiteDatabase db);
- }
-
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java
deleted file mode 100644
index c4ff4bd..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * 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 android.arch.persistence.db.SupportSQLiteProgram;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.support.annotation.IntDef;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * This class is used as an intermediate place to keep binding arguments so that we can run
- * Cursor queries with correct types rather than passing everything as a string.
- * <p>
- * Because it is relatively a big object, they are pooled and must be released after each use.
- *
- * @hide
- */
-@SuppressWarnings("unused")
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- // Maximum number of queries we'll keep cached.
- static final int POOL_LIMIT = 15;
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
- // clear the bigger queries (# of arguments).
- static final int DESIRED_POOL_SIZE = 10;
- private volatile String mQuery;
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- final long[] mLongBindings;
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- final double[] mDoubleBindings;
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- final String[] mStringBindings;
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- final byte[][] mBlobBindings;
-
- @Binding
- private final int[] mBindingTypes;
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- final int mCapacity;
- // number of arguments in the query
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- int mArgCount;
-
-
- @SuppressWarnings("WeakerAccess")
- @VisibleForTesting
- static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
-
- /**
- * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
- *
- * @param supportSQLiteQuery The query to copy from
- * @return A new query copied from the provided one.
- */
- public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
- final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
- supportSQLiteQuery.getSql(),
- supportSQLiteQuery.getArgCount());
- supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
- @Override
- public void bindNull(int index) {
- query.bindNull(index);
- }
-
- @Override
- public void bindLong(int index, long value) {
- query.bindLong(index, value);
- }
-
- @Override
- public void bindDouble(int index, double value) {
- query.bindDouble(index, value);
- }
-
- @Override
- public void bindString(int index, String value) {
- query.bindString(index, value);
- }
-
- @Override
- public void bindBlob(int index, byte[] value) {
- query.bindBlob(index, value);
- }
-
- @Override
- public void clearBindings() {
- query.clearBindings();
- }
-
- @Override
- public void close() {
- // ignored.
- }
- });
- return query;
- }
-
- /**
- * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
- * given query.
- *
- * @param query The query to prepare
- * @param argumentCount The number of query arguments
- * @return A RoomSQLiteQuery that holds the given query and has space for the given number of
- * arguments.
- */
- @SuppressWarnings("WeakerAccess")
- public static RoomSQLiteQuery acquire(String query, int argumentCount) {
- synchronized (sQueryPool) {
- final Map.Entry<Integer, RoomSQLiteQuery> entry =
- sQueryPool.ceilingEntry(argumentCount);
- if (entry != null) {
- sQueryPool.remove(entry.getKey());
- final RoomSQLiteQuery sqliteQuery = entry.getValue();
- sqliteQuery.init(query, argumentCount);
- return sqliteQuery;
- }
- }
- RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
- sqLiteQuery.init(query, argumentCount);
- return sqLiteQuery;
- }
-
- private RoomSQLiteQuery(int capacity) {
- mCapacity = capacity;
- // because, 1 based indices... we don't want to offsets everything with 1 all the time.
- int limit = capacity + 1;
- //noinspection WrongConstant
- mBindingTypes = new int[limit];
- mLongBindings = new long[limit];
- mDoubleBindings = new double[limit];
- mStringBindings = new String[limit];
- mBlobBindings = new byte[limit][];
- }
-
- @SuppressWarnings("WeakerAccess")
- void init(String query, int argCount) {
- mQuery = query;
- mArgCount = argCount;
- }
-
- /**
- * Releases the query back to the pool.
- * <p>
- * After released, the statement might be returned when {@link #acquire(String, int)} is called
- * so you should never re-use it after releasing.
- */
- @SuppressWarnings("WeakerAccess")
- public void release() {
- synchronized (sQueryPool) {
- sQueryPool.put(mCapacity, this);
- prunePoolLocked();
- }
- }
-
- private static void prunePoolLocked() {
- if (sQueryPool.size() > POOL_LIMIT) {
- int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
- final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
- while (toBeRemoved-- > 0) {
- iterator.next();
- iterator.remove();
- }
- }
- }
-
- @Override
- public String getSql() {
- return mQuery;
- }
-
- @Override
- public int getArgCount() {
- return mArgCount;
- }
-
- @Override
- public void bindTo(SupportSQLiteProgram program) {
- for (int index = 1; index <= mArgCount; index++) {
- switch (mBindingTypes[index]) {
- case NULL:
- program.bindNull(index);
- break;
- case LONG:
- program.bindLong(index, mLongBindings[index]);
- break;
- case DOUBLE:
- program.bindDouble(index, mDoubleBindings[index]);
- break;
- case STRING:
- program.bindString(index, mStringBindings[index]);
- break;
- case BLOB:
- program.bindBlob(index, mBlobBindings[index]);
- break;
- }
- }
- }
-
- @Override
- public void bindNull(int index) {
- mBindingTypes[index] = NULL;
- }
-
- @Override
- public void bindLong(int index, long value) {
- mBindingTypes[index] = LONG;
- mLongBindings[index] = value;
- }
-
- @Override
- public void bindDouble(int index, double value) {
- mBindingTypes[index] = DOUBLE;
- mDoubleBindings[index] = value;
- }
-
- @Override
- public void bindString(int index, String value) {
- mBindingTypes[index] = STRING;
- mStringBindings[index] = value;
- }
-
- @Override
- public void bindBlob(int index, byte[] value) {
- mBindingTypes[index] = BLOB;
- mBlobBindings[index] = value;
- }
-
- @Override
- public void close() {
- // no-op. not calling release because it is internal API.
- }
-
- /**
- * Copies arguments from another RoomSQLiteQuery into this query.
- *
- * @param other The other query, which holds the arguments to be copied.
- */
- public void copyArgumentsFrom(RoomSQLiteQuery other) {
- int argCount = other.getArgCount() + 1; // +1 for the binding offsets
- System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
- System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
- System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
- System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
- System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
- }
-
- @Override
- public void clearBindings() {
- Arrays.fill(mBindingTypes, NULL);
- Arrays.fill(mStringBindings, null);
- Arrays.fill(mBlobBindings, null);
- mQuery = null;
- // no need to clear others
- }
-
- private static final int NULL = 1;
- private static final int LONG = 2;
- private static final int DOUBLE = 3;
- private static final int STRING = 4;
- private static final int BLOB = 5;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
- @interface Binding {
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/SharedSQLiteStatement.java b/room/runtime/src/main/java/android/arch/persistence/room/SharedSQLiteStatement.java
deleted file mode 100644
index 6b1f8ea..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/SharedSQLiteStatement.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.arch.persistence.db.SupportSQLiteStatement;
-import android.support.annotation.RestrictTo;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Represents a prepared SQLite state that can be re-used multiple times.
- * <p>
- * This class is used by generated code. After it is used, {@code release} must be called so that
- * it can be used by other threads.
- * <p>
- * To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
- * statement until it is released.
- *
- * @hide
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class SharedSQLiteStatement {
- private final AtomicBoolean mLock = new AtomicBoolean(false);
-
- private final RoomDatabase mDatabase;
- private volatile SupportSQLiteStatement mStmt;
-
- /**
- * Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
- * it automatically creates a new one.
- *
- * @param database The database to create the statement in.
- */
- public SharedSQLiteStatement(RoomDatabase database) {
- mDatabase = database;
- }
-
- /**
- * Create the query.
- *
- * @return The SQL query to prepare.
- */
- protected abstract String createQuery();
-
- protected void assertNotMainThread() {
- mDatabase.assertNotMainThread();
- }
-
- private SupportSQLiteStatement createNewStatement() {
- String query = createQuery();
- return mDatabase.compileStatement(query);
- }
-
- private SupportSQLiteStatement getStmt(boolean canUseCached) {
- final SupportSQLiteStatement stmt;
- if (canUseCached) {
- if (mStmt == null) {
- mStmt = createNewStatement();
- }
- stmt = mStmt;
- } else {
- // it is in use, create a one off statement
- stmt = createNewStatement();
- }
- return stmt;
- }
-
- /**
- * Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
- */
- public SupportSQLiteStatement acquire() {
- assertNotMainThread();
- return getStmt(mLock.compareAndSet(false, true));
- }
-
- /**
- * Must call this when statement will not be used anymore.
- *
- * @param statement The statement that was returned from acquire.
- */
- public void release(SupportSQLiteStatement statement) {
- if (statement == mStmt) {
- mLock.set(false);
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java b/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java
deleted file mode 100644
index d69ea0d..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.migration;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.support.annotation.NonNull;
-
-/**
- * Base class for a database migration.
- * <p>
- * Each migration can move between 2 versions that are defined by {@link #startVersion} and
- * {@link #endVersion}.
- * <p>
- * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
- * going version 3 to 5 without going to version 4). If Room opens a database at version
- * 3 and latest version is >= 5, Room will use the migration object that can migrate from
- * 3 to 5 instead of 3 to 4 and 4 to 5.
- * <p>
- * If there are not enough migrations provided to move from the current version to the latest
- * version, Room will clear the database and recreate so even if you have no changes between 2
- * versions, you should still provide a Migration object to the builder.
- */
-public abstract class Migration {
- public final int startVersion;
- public final int endVersion;
-
- /**
- * Creates a new migration between {@code startVersion} and {@code endVersion}.
- *
- * @param startVersion The start version of the database.
- * @param endVersion The end version of the database after this migration is applied.
- */
- public Migration(int startVersion, int endVersion) {
- this.startVersion = startVersion;
- this.endVersion = endVersion;
- }
-
- /**
- * Should run the necessary migrations.
- * <p>
- * This class cannot access any generated Dao in this method.
- * <p>
- * This method is already called inside a transaction and that transaction might actually be a
- * composite transaction of all necessary {@code Migration}s.
- *
- * @param database The database instance
- */
- public abstract void migrate(@NonNull SupportSQLiteDatabase database);
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/package-info.java b/room/runtime/src/main/java/android/arch/persistence/room/package-info.java
deleted file mode 100644
index 1dafc1b..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/package-info.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * Room is a Database Object Mapping library that makes it easy to access database on Android
- * applications.
- * <p>
- * Rather than hiding the detail of SQLite, Room tries to embrace them by providing convenient APIs
- * to query the database and also verify such queries at compile time. This allows you to access
- * the full power of SQLite while having the type safety provided by Java SQL query builders.
- * <p>
- * There are 3 major components in Room.
- * <ul>
- * <li>{@link android.arch.persistence.room.Database Database}: This annotation marks a
- * class as a database. It should be an abstract class that extends
- * {@link android.arch.persistence.room.RoomDatabase RoomDatabase}. At runtime, you can acquire
- * an instance of it via {@link android.arch.persistence.room.Room#databaseBuilder(
- * android.content.Context,java.lang.Class, java.lang.String) Room.databaseBuilder} or
- * {@link android.arch.persistence.room.Room#inMemoryDatabaseBuilder(android.content.Context,
- * java.lang.Class) Room.inMemoryDatabaseBuilder}.
- * <p>
- * This class defines the list of entities and data access objects in the database. It is
- * also the main access point for the underlying connection.
- * </li>
- * <li>{@link android.arch.persistence.room.Entity Entity}: This annotation marks a class as a
- * 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 (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
- * responsible for defining the methods that access the database. The class that is annotated
- * with {@link android.arch.persistence.room.Database Database} must have an abstract method
- * that has 0 arguments and returns the class that is annotated with Dao. While generating the
- * code at compile time, Room will generate an implementation of this class.
- * <pre>
- * Using Dao classes for database access rather than query builders or direct queries allows you
- * to keep a separation between different components and easily mock the database access while
- * testing your application.
- * </li>
- * </ul>
- * Below is a sample of a simple database.
- * <pre>
- * // File: User.java
- * {@literal @}Entity
- * public class User {
- * {@literal @}PrimaryKey
- * private int uid;
- * private String name;
- * {@literal @}ColumnInfo(name = "last_name")
- * private String lastName;
- * // getters and setters are ignored for brevity but they are required for Room to work.
- * }
- * // File: UserDao.java
- * {@literal @}Dao
- * public interface UserDao {
- * {@literal @}Query("SELECT * FROM user")
- * List<User> loadAll();
- * {@literal @}Query("SELECT * FROM user WHERE uid IN (:userIds)")
- * List<User> loadAllByUserId(int... userIds);
- * {@literal @}Query("SELECT * FROM user where name LIKE :first AND last_name LIKE :last LIMIT 1")
- * User loadOneByNameAndLastName(String first, String last);
- * {@literal @}Insert
- * void insertAll(User... users);
- * {@literal @}Delete
- * void delete(User user);
- * }
- * // File: AppDatabase.java
- * {@literal @}Database(entities = {User.java})
- * public abstract class AppDatabase extends RoomDatabase {
- * public abstract UserDao userDao();
- * }
- * </pre>
- * You can create an instance of {@code AppDatabase} as follows:
- * <pre>
- * AppDatabase db = Room
- * .databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name")
- * .build();
- * </pre>
- * Since Room verifies your queries at compile time, it also detects information about which tables
- * are accessed by the query or what columns are present in the response.
- * <p>
- * You can observe a particular table for changes using the
- * {@link android.arch.persistence.room.InvalidationTracker InvalidationTracker} class which you can
- * acquire via {@link android.arch.persistence.room.RoomDatabase#getInvalidationTracker()
- * RoomDatabase.getInvalidationTracker}.
- * <p>
- * For convenience, Room allows you to return {@link android.arch.lifecycle.LiveData
- * LiveData} from {@link android.arch.persistence.room.Query Query} methods. It will automatically
- * observe the related tables as long as the {@code LiveData} has active observers.
- * <pre>
- * // This live data will automatically dispatch changes as the database changes.
- * {@literal @}Query("SELECT * FROM user ORDER BY name LIMIT 5")
- * LiveData<User> loadFirstFiveUsers();
- * </pre>
- * <p>
- * You can also return arbitrary Java objects from your query results as long as the fields in the
- * object match the list of columns in the query response. This makes it very easy to write
- * applications that drive the UI from persistent storage.
- * <pre>
- * class IdAndFullName {
- * public int uid;
- * {@literal @}ColumnInfo(name = "full_name")
- * public String fullName;
- * }
- * // DAO
- * {@literal @}Query("SELECT uid, name || lastName as full_name FROM user")
- * public IdAndFullName[] loadFullNames();
- * </pre>
- * If there is a mismatch between the query result and the POJO, Room will print a warning during
- * compilation.
- * <p>
- * Please see the documentation of individual classes for details.
- */
-package android.arch.persistence.room;
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
deleted file mode 100644
index 73777c4..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * 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.paging;
-
-import android.arch.paging.PositionalDataSource;
-import android.arch.persistence.db.SupportSQLiteQuery;
-import android.arch.persistence.room.InvalidationTracker;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.RoomSQLiteQuery;
-import android.database.Cursor;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-/**
- * A simple data source implementation that uses Limit & Offset to page the query.
- * <p>
- * This is NOT the most efficient way to do paging on SQLite. It is
- * <a href="http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor">recommended</a> to use an indexed
- * ORDER BY statement but that requires a more complex API. This solution is technically equal to
- * receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
- * never returns inconsistent data if it is invalidated.
- *
- * @param <T> Data type returned by the data source.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> {
- private final RoomSQLiteQuery mSourceQuery;
- private final String mCountQuery;
- private final String mLimitOffsetQuery;
- private final RoomDatabase mDb;
- @SuppressWarnings("FieldCanBeLocal")
- private final InvalidationTracker.Observer mObserver;
- private final boolean mInTransaction;
-
- protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
- boolean inTransaction, String... tables) {
- this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, tables);
- }
-
- protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
- boolean inTransaction, String... tables) {
- mDb = db;
- mSourceQuery = query;
- mInTransaction = inTransaction;
- mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
- mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
- mObserver = new InvalidationTracker.Observer(tables) {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- invalidate();
- }
- };
- db.getInvalidationTracker().addWeakObserver(mObserver);
- }
-
- /**
- * Count number of rows query can return
- */
- @SuppressWarnings("WeakerAccess")
- public int countItems() {
- final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
- mSourceQuery.getArgCount());
- sqLiteQuery.copyArgumentsFrom(mSourceQuery);
- Cursor cursor = mDb.query(sqLiteQuery);
- try {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- return 0;
- } finally {
- cursor.close();
- sqLiteQuery.release();
- }
- }
-
- @Override
- public boolean isInvalid() {
- mDb.getInvalidationTracker().refreshVersionsSync();
- return super.isInvalid();
- }
-
- @SuppressWarnings("WeakerAccess")
- protected abstract List<T> convertRows(Cursor cursor);
-
- @Override
- public void loadInitial(@NonNull LoadInitialParams params,
- @NonNull LoadInitialCallback<T> callback) {
- int totalCount = countItems();
- if (totalCount == 0) {
- callback.onResult(Collections.<T>emptyList(), 0, 0);
- return;
- }
-
- // bound the size requested, based on known count
- final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
- final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
-
- List<T> list = loadRange(firstLoadPosition, firstLoadSize);
- if (list != null && list.size() == firstLoadSize) {
- callback.onResult(list, firstLoadPosition, totalCount);
- } else {
- // null list, or size doesn't match request - DB modified between count and load
- invalidate();
- }
- }
-
- @Override
- public void loadRange(@NonNull LoadRangeParams params,
- @NonNull LoadRangeCallback<T> callback) {
- List<T> list = loadRange(params.startPosition, params.loadSize);
- if (list != null) {
- callback.onResult(list);
- } else {
- invalidate();
- }
- }
-
- /**
- * Return the rows from startPos to startPos + loadCount
- */
- @Nullable
- public List<T> loadRange(int startPosition, int loadCount) {
- final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
- mSourceQuery.getArgCount() + 2);
- sqLiteQuery.copyArgumentsFrom(mSourceQuery);
- sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
- sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
- if (mInTransaction) {
- mDb.beginTransaction();
- Cursor cursor = null;
- try {
- cursor = mDb.query(sqLiteQuery);
- List<T> rows = convertRows(cursor);
- mDb.setTransactionSuccessful();
- return rows;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- mDb.endTransaction();
- sqLiteQuery.release();
- }
- } else {
- Cursor cursor = mDb.query(sqLiteQuery);
- //noinspection TryFinallyCanBeTryWithResources
- try {
- return convertRows(cursor);
- } finally {
- cursor.close();
- sqLiteQuery.release();
- }
- }
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/util/StringUtil.java b/room/runtime/src/main/java/android/arch/persistence/room/util/StringUtil.java
deleted file mode 100644
index d01e3c5..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/util/StringUtil.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2016 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.util;
-
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.StringTokenizer;
-
-/**
- * @hide
- *
- * String utilities for Room
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class StringUtil {
-
- @SuppressWarnings("unused")
- public static final String[] EMPTY_STRING_ARRAY = new String[0];
- /**
- * Returns a new StringBuilder to be used while producing SQL queries.
- *
- * @return A new or recycled StringBuilder
- */
- public static StringBuilder newStringBuilder() {
- // TODO pool:
- return new StringBuilder();
- }
-
- /**
- * Adds bind variable placeholders (?) to the given string. Each placeholder is separated
- * by a comma.
- *
- * @param builder The StringBuilder for the query
- * @param count Number of placeholders
- */
- public static void appendPlaceholders(StringBuilder builder, int count) {
- for (int i = 0; i < count; i++) {
- builder.append("?");
- if (i < count - 1) {
- builder.append(",");
- }
- }
- }
- /**
- * Splits a comma separated list of integers to integer list.
- * <p>
- * If an input is malformed, it is omitted from the result.
- *
- * @param input Comma separated list of integers.
- * @return A List containing the integers or null if the input is null.
- */
- @Nullable
- public static List<Integer> splitToIntList(@Nullable String input) {
- if (input == null) {
- return null;
- }
- List<Integer> result = new ArrayList<>();
- StringTokenizer tokenizer = new StringTokenizer(input, ",");
- while (tokenizer.hasMoreElements()) {
- final String item = tokenizer.nextToken();
- try {
- result.add(Integer.parseInt(item));
- } catch (NumberFormatException ex) {
- Log.e("ROOM", "Malformed integer list", ex);
- }
- }
- return result;
- }
-
- /**
- * Joins the given list of integers into a comma separated list.
- *
- * @param input The list of integers.
- * @return Comma separated string composed of integers in the list. If the list is null, return
- * value is null.
- */
- @Nullable
- public static String joinIntoString(@Nullable List<Integer> input) {
- if (input == null) {
- return null;
- }
-
- final int size = input.size();
- if (size == 0) {
- return "";
- }
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < size; i++) {
- sb.append(Integer.toString(input.get(i)));
- if (i < size - 1) {
- sb.append(",");
- }
- }
- return sb.toString();
- }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java b/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java
deleted file mode 100644
index 19d9853..0000000
--- a/room/runtime/src/main/java/android/arch/persistence/room/util/TableInfo.java
+++ /dev/null
@@ -1,591 +0,0 @@
-/*
- * 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.util;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.room.ColumnInfo;
-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;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-/**
- * A data class that holds the information about a table.
- * <p>
- * It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
- * <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
- * documentation for more details.
- * <p>
- * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
- "SimplifiableIfStatement"})
-// if you change this class, you must change TableInfoWriter.kt
-public class TableInfo {
- /**
- * The table name.
- */
- public final String name;
- /**
- * Unmodifiable map of columns keyed by column name.
- */
- public final Map<String, Column> columns;
-
- 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,
- 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
- + '}';
- }
-
- /**
- * Reads the table information from the given database.
- *
- * @param database The database to read the information from.
- * @param tableName The table name.
- * @return A TableInfo containing the schema information for the provided table name.
- */
- @SuppressWarnings("SameParameterValue")
- public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
- Map<String, Column> columns = readColumns(database, tableName);
- Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
- Set<Index> indices = readIndices(database, tableName);
- return new TableInfo(tableName, columns, foreignKeys, indices);
- }
-
- private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
- String tableName) {
- Set<ForeignKey> foreignKeys = new HashSet<>();
- // this seems to return everything in order but it is not documented so better be safe
- Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
- try {
- final int idColumnIndex = cursor.getColumnIndex("id");
- final int seqColumnIndex = cursor.getColumnIndex("seq");
- final int tableColumnIndex = cursor.getColumnIndex("table");
- final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
- final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
-
- final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor);
- final int count = cursor.getCount();
- for (int position = 0; position < count; position++) {
- cursor.moveToPosition(position);
- final int seq = cursor.getInt(seqColumnIndex);
- if (seq != 0) {
- continue;
- }
- final int id = cursor.getInt(idColumnIndex);
- List<String> myColumns = new ArrayList<>();
- List<String> refColumns = new ArrayList<>();
- for (ForeignKeyWithSequence key : ordered) {
- if (key.mId == id) {
- myColumns.add(key.mFrom);
- refColumns.add(key.mTo);
- }
- }
- foreignKeys.add(new ForeignKey(
- cursor.getString(tableColumnIndex),
- cursor.getString(onDeleteColumnIndex),
- cursor.getString(onUpdateColumnIndex),
- myColumns,
- refColumns
- ));
- }
- } finally {
- cursor.close();
- }
- return foreignKeys;
- }
-
- private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) {
- final int idColumnIndex = cursor.getColumnIndex("id");
- final int seqColumnIndex = cursor.getColumnIndex("seq");
- final int fromColumnIndex = cursor.getColumnIndex("from");
- final int toColumnIndex = cursor.getColumnIndex("to");
- final int count = cursor.getCount();
- List<ForeignKeyWithSequence> result = new ArrayList<>();
- for (int i = 0; i < count; i++) {
- cursor.moveToPosition(i);
- result.add(new ForeignKeyWithSequence(
- cursor.getInt(idColumnIndex),
- cursor.getInt(seqColumnIndex),
- cursor.getString(fromColumnIndex),
- cursor.getString(toColumnIndex)
- ));
- }
- Collections.sort(result);
- return result;
- }
-
- private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
- String tableName) {
- Cursor cursor = database
- .query("PRAGMA table_info(`" + tableName + "`)");
- //noinspection TryFinallyCanBeTryWithResources
- Map<String, Column> columns = new HashMap<>();
- try {
- if (cursor.getColumnCount() > 0) {
- int nameIndex = cursor.getColumnIndex("name");
- int typeIndex = cursor.getColumnIndex("type");
- int notNullIndex = cursor.getColumnIndex("notnull");
- int pkIndex = cursor.getColumnIndex("pk");
-
- while (cursor.moveToNext()) {
- final String name = cursor.getString(nameIndex);
- final String type = cursor.getString(typeIndex);
- final boolean notNull = 0 != cursor.getInt(notNullIndex);
- final int primaryKeyPosition = cursor.getInt(pkIndex);
- columns.put(name, new Column(name, type, notNull, primaryKeyPosition));
- }
- }
- } finally {
- cursor.close();
- }
- return columns;
- }
-
- /**
- * @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();
- }
- }
-
- /**
- * @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<>();
-
- 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();
- }
- }
-
- /**
- * Holds the information about a database column.
- */
- @SuppressWarnings("WeakerAccess")
- public static class Column {
- /**
- * The column name.
- */
- public final String name;
- /**
- * The column type affinity.
- */
- public final String type;
- /**
- * The column type after it is normalized to one of the basic types according to
- * https://www.sqlite.org/datatype3.html Section 3.1.
- * <p>
- * This is the value Room uses for equality check.
- */
- @ColumnInfo.SQLiteTypeAffinity
- public final int affinity;
- /**
- * Whether or not the column can be NULL.
- */
- public final boolean notNull;
- /**
- * The position of the column in the list of primary keys, 0 if the column is not part
- * of the primary key.
- * <p>
- * This information is only available in API 20+.
- * <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
- * On older platforms, it will be 1 if the column is part of the primary key and 0
- * otherwise.
- * <p>
- * The {@link #equals(Object)} implementation handles this inconsistency based on
- * API levels os if you are using a custom SQLite deployment, it may return false
- * positives.
- */
- public final int primaryKeyPosition;
-
- // if you change this constructor, you must change TableInfoWriter.kt
- public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
- this.name = name;
- this.type = type;
- this.notNull = notNull;
- this.primaryKeyPosition = primaryKeyPosition;
- this.affinity = findAffinity(type);
- }
-
- /**
- * Implements https://www.sqlite.org/datatype3.html section 3.1
- *
- * @param type The type that was given to the sqlite
- * @return The normalized type which is one of the 5 known affinities
- */
- @ColumnInfo.SQLiteTypeAffinity
- private static int findAffinity(@Nullable String type) {
- if (type == null) {
- return ColumnInfo.BLOB;
- }
- String uppercaseType = type.toUpperCase(Locale.US);
- if (uppercaseType.contains("INT")) {
- return ColumnInfo.INTEGER;
- }
- if (uppercaseType.contains("CHAR")
- || uppercaseType.contains("CLOB")
- || uppercaseType.contains("TEXT")) {
- return ColumnInfo.TEXT;
- }
- if (uppercaseType.contains("BLOB")) {
- return ColumnInfo.BLOB;
- }
- if (uppercaseType.contains("REAL")
- || uppercaseType.contains("FLOA")
- || uppercaseType.contains("DOUB")) {
- return ColumnInfo.REAL;
- }
- // sqlite returns NUMERIC here but it is like a catch all. We already
- // have UNDEFINED so it is better to use UNDEFINED for consistency.
- return ColumnInfo.UNDEFINED;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Column column = (Column) o;
- if (Build.VERSION.SDK_INT >= 20) {
- if (primaryKeyPosition != column.primaryKeyPosition) return false;
- } else {
- if (isPrimaryKey() != column.isPrimaryKey()) return false;
- }
-
- if (!name.equals(column.name)) return false;
- //noinspection SimplifiableIfStatement
- if (notNull != column.notNull) return false;
- return affinity == column.affinity;
- }
-
- /**
- * Returns whether this column is part of the primary key or not.
- *
- * @return True if this column is part of the primary key, false otherwise.
- */
- public boolean isPrimaryKey() {
- return primaryKeyPosition > 0;
- }
-
- @Override
- public int hashCode() {
- int result = name.hashCode();
- result = 31 * result + affinity;
- result = 31 * result + (notNull ? 1231 : 1237);
- result = 31 * result + primaryKeyPosition;
- return result;
- }
-
- @Override
- public String toString() {
- return "Column{"
- + "name='" + name + '\''
- + ", type='" + type + '\''
- + ", affinity='" + affinity + '\''
- + ", notNull=" + notNull
- + ", primaryKeyPosition=" + primaryKeyPosition
- + '}';
- }
- }
-
- /**
- * Holds the information about an SQLite foreign key
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static class ForeignKey {
- @NonNull
- public final String referenceTable;
- @NonNull
- public final String onDelete;
- @NonNull
- public final String onUpdate;
- @NonNull
- public final List<String> columnNames;
- @NonNull
- public final List<String> referenceColumnNames;
-
- public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
- @NonNull String onUpdate,
- @NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) {
- this.referenceTable = referenceTable;
- this.onDelete = onDelete;
- this.onUpdate = onUpdate;
- this.columnNames = Collections.unmodifiableList(columnNames);
- this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- ForeignKey that = (ForeignKey) o;
-
- if (!referenceTable.equals(that.referenceTable)) return false;
- if (!onDelete.equals(that.onDelete)) return false;
- if (!onUpdate.equals(that.onUpdate)) return false;
- //noinspection SimplifiableIfStatement
- if (!columnNames.equals(that.columnNames)) return false;
- return referenceColumnNames.equals(that.referenceColumnNames);
- }
-
- @Override
- public int hashCode() {
- int result = referenceTable.hashCode();
- result = 31 * result + onDelete.hashCode();
- result = 31 * result + onUpdate.hashCode();
- result = 31 * result + columnNames.hashCode();
- result = 31 * result + referenceColumnNames.hashCode();
- return result;
- }
-
- @Override
- public String toString() {
- return "ForeignKey{"
- + "referenceTable='" + referenceTable + '\''
- + ", onDelete='" + onDelete + '\''
- + ", onUpdate='" + onUpdate + '\''
- + ", columnNames=" + columnNames
- + ", referenceColumnNames=" + referenceColumnNames
- + '}';
- }
- }
-
- /**
- * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
- * sorting in the generated foreign key object.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> {
- final int mId;
- final int mSequence;
- final String mFrom;
- final String mTo;
-
- ForeignKeyWithSequence(int id, int sequence, String from, String to) {
- mId = id;
- mSequence = sequence;
- mFrom = from;
- mTo = to;
- }
-
- @Override
- public int compareTo(@NonNull ForeignKeyWithSequence o) {
- final int idCmp = mId - o.mId;
- if (idCmp == 0) {
- return mSequence - o.mSequence;
- } else {
- return idCmp;
- }
- }
- }
-
- /**
- * 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
- + '}';
- }
- }
-}
diff --git a/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java b/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
new file mode 100644
index 0000000..8bc1d4f
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Configuration class for a {@link RoomDatabase}.
+ */
+@SuppressWarnings("WeakerAccess")
+public class DatabaseConfiguration {
+ /**
+ * The factory to use to access the database.
+ */
+ @NonNull
+ public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ /**
+ * The context to use while connecting to the database.
+ */
+ @NonNull
+ public final Context context;
+ /**
+ * The name of the database file or null if it is an in-memory database.
+ */
+ @Nullable
+ public final String name;
+
+ /**
+ * Collection of available migrations.
+ */
+ @NonNull
+ public final RoomDatabase.MigrationContainer migrationContainer;
+
+ @Nullable
+ public final List<RoomDatabase.Callback> callbacks;
+
+ /**
+ * Whether Room should throw an exception for queries run on the main thread.
+ */
+ public final boolean allowMainThreadQueries;
+
+ /**
+ * The journal mode for this database.
+ */
+ public final RoomDatabase.JournalMode journalMode;
+
+ /**
+ * If true, Room should crash if a migration is missing.
+ */
+ public final boolean requireMigration;
+
+ /**
+ * The collection of schema versions from which migrations aren't required.
+ */
+ private final Set<Integer> mMigrationNotRequiredFrom;
+
+ /**
+ * Creates a database configuration with the given values.
+ *
+ * @param context The application context.
+ * @param name Name of the database, can be null if it is in memory.
+ * @param sqliteOpenHelperFactory The open helper factory to use.
+ * @param migrationContainer The migration container for migrations.
+ * @param callbacks The list of callbacks for database events.
+ * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
+ * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
+ * @param requireMigration True if Room should require a valid migration if version changes,
+ * instead of recreating the tables.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
+ @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
+ @NonNull RoomDatabase.MigrationContainer migrationContainer,
+ @Nullable List<RoomDatabase.Callback> callbacks,
+ boolean allowMainThreadQueries,
+ RoomDatabase.JournalMode journalMode,
+ boolean requireMigration,
+ @Nullable Set<Integer> migrationNotRequiredFrom) {
+ this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
+ this.context = context;
+ this.name = name;
+ this.migrationContainer = migrationContainer;
+ this.callbacks = callbacks;
+ this.allowMainThreadQueries = allowMainThreadQueries;
+ this.journalMode = journalMode;
+ this.requireMigration = requireMigration;
+ this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
+ }
+
+ /**
+ * Returns whether a migration is required from the specified version.
+ *
+ * @param version The schema version.
+ * @return True if a valid migration is required, false otherwise.
+ */
+ public boolean isMigrationRequiredFrom(int version) {
+ // Migrations are required from this version if we generally require migrations AND EITHER
+ // there are no exceptions OR the supplied version is not one of the exceptions.
+ return requireMigration
+ && (mMigrationNotRequiredFrom == null
+ || !mMigrationNotRequiredFrom.contains(version));
+
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java b/room/runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java
new file mode 100644
index 0000000..098a14b
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+/**
+ * Implementations of this class knows how to delete or update a particular entity.
+ * <p>
+ * This is an internal library class and all of its implementations are auto-generated.
+ *
+ * @param <T> The type parameter of the entity to be deleted
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressWarnings({"WeakerAccess", "unused"})
+public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
+ /**
+ * Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
+ * database.
+ *
+ * @param database The database to delete / update the item in.
+ */
+ public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
+ super(database);
+ }
+
+ /**
+ * Create the deletion or update query
+ *
+ * @return An SQL query that can delete or update instances of T.
+ */
+ @Override
+ protected abstract String createQuery();
+
+ /**
+ * Binds the entity into the given statement.
+ *
+ * @param statement The SQLite statement that prepared for the query returned from
+ * createQuery.
+ * @param entity The entity of type T.
+ */
+ protected abstract void bind(SupportSQLiteStatement statement, T entity);
+
+ /**
+ * Deletes or updates the given entities in the database and returns the affected row count.
+ *
+ * @param entity The entity to delete or update
+ * @return The number of affected rows
+ */
+ public final int handle(T entity) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ bind(stmt, entity);
+ return stmt.executeUpdateDelete();
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Deletes or updates the given entities in the database and returns the affected row count.
+ *
+ * @param entities Entities to delete or update
+ * @return The number of affected rows
+ */
+ public final int handleMultiple(Iterable<T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ int total = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ total += stmt.executeUpdateDelete();
+ }
+ return total;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Deletes or updates the given entities in the database and returns the affected row count.
+ *
+ * @param entities Entities to delete or update
+ * @return The number of affected rows
+ */
+ public final int handleMultiple(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ int total = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ total += stmt.executeUpdateDelete();
+ }
+ return total;
+ } finally {
+ release(stmt);
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/EntityInsertionAdapter.java b/room/runtime/src/main/java/androidx/room/EntityInsertionAdapter.java
new file mode 100644
index 0000000..97f3969
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/EntityInsertionAdapter.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Implementations of this class knows how to insert a particular entity.
+ * <p>
+ * This is an internal library class and all of its implementations are auto-generated.
+ *
+ * @param <T> The type parameter of the entity to be inserted
+ * @hide
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
+ /**
+ * Creates an InsertionAdapter that can insert the entity type T into the given database.
+ *
+ * @param database The database to insert into.
+ */
+ public EntityInsertionAdapter(RoomDatabase database) {
+ super(database);
+ }
+
+ /**
+ * Binds the entity into the given statement.
+ *
+ * @param statement The SQLite statement that prepared for the query returned from
+ * createInsertQuery.
+ * @param entity The entity of type T.
+ */
+ protected abstract void bind(SupportSQLiteStatement statement, T entity);
+
+ /**
+ * Inserts the entity into the database.
+ *
+ * @param entity The entity to insert
+ */
+ public final void insert(T entity) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ bind(stmt, entity);
+ stmt.executeInsert();
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database.
+ *
+ * @param entities Entities to insert
+ */
+ public final void insert(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ for (T entity : entities) {
+ bind(stmt, entity);
+ stmt.executeInsert();
+ }
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database.
+ *
+ * @param entities Entities to insert
+ */
+ public final void insert(Iterable<T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ for (T entity : entities) {
+ bind(stmt, entity);
+ stmt.executeInsert();
+ }
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entity into the database and returns the row id.
+ *
+ * @param entity The entity to insert
+ * @return The SQLite row id
+ */
+ public final long insertAndReturnId(T entity) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ bind(stmt, entity);
+ return stmt.executeInsert();
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids
+ */
+ public final long[] insertAndReturnIdsArray(Collection<T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final long[] result = new long[entities.size()];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids
+ */
+ public final long[] insertAndReturnIdsArray(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final long[] result = new long[entities.length];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids
+ */
+ public final Long[] insertAndReturnIdsArrayBox(Collection<T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final Long[] result = new Long[entities.size()];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids
+ */
+ public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final Long[] result = new Long[entities.length];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids
+ */
+ public final List<Long> insertAndReturnIdsList(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final List<Long> result = new ArrayList<>(entities.length);
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result.add(index, stmt.executeInsert());
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids
+ */
+ public final List<Long> insertAndReturnIdsList(Collection<T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final List<Long> result = new ArrayList<>(entities.size());
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result.add(index, stmt.executeInsert());
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
new file mode 100644
index 0000000..801f5a3
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
@@ -0,0 +1,692 @@
+/*
+ * 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 androidx.room;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.internal.SafeIterableMap;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
+ * these tables.
+ */
+// We create an in memory table with (version, table_id) where version is an auto-increment primary
+// key and a table_id (hardcoded int from initialization).
+// ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
+// Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
+// After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
+// tables.
+// Each update on one of the observed tables triggers an insertion into this table, hence a
+// new version.
+// Unfortunately, we cannot override the previous row because sqlite uses the conflict resolution
+// of the outer query (the thing that triggered us) so we do a cleanup as we sync instead of letting
+// SQLite override the rows.
+// https://sqlite.org/lang_createtrigger.html: An ON CONFLICT clause may be specified as part of an
+// UPDATE or INSERT action within the body of the trigger. However if an ON CONFLICT clause is
+// specified as part of the statement causing the trigger to fire, then conflict handling policy of
+// the outer statement is used instead.
+public class InvalidationTracker {
+
+ private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
+
+ private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
+
+ private static final String VERSION_COLUMN_NAME = "version";
+
+ private static final String TABLE_ID_COLUMN_NAME = "table_id";
+
+ private static final String CREATE_VERSION_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
+ + "(" + VERSION_COLUMN_NAME
+ + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + TABLE_ID_COLUMN_NAME
+ + " INTEGER)";
+
+ @VisibleForTesting
+ static final String CLEANUP_SQL = "DELETE FROM " + UPDATE_TABLE_NAME
+ + " WHERE " + VERSION_COLUMN_NAME + " NOT IN( SELECT MAX("
+ + VERSION_COLUMN_NAME + ") FROM " + UPDATE_TABLE_NAME
+ + " GROUP BY " + TABLE_ID_COLUMN_NAME + ")";
+
+ @VisibleForTesting
+ // We always clean before selecting so it is unlikely to have the same row twice and if we
+ // do, it is not a big deal, just more data in the cursor.
+ static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
+ + " WHERE " + VERSION_COLUMN_NAME
+ + " > ? ORDER BY " + VERSION_COLUMN_NAME + " ASC;";
+
+ @NonNull
+ @VisibleForTesting
+ ArrayMap<String, Integer> mTableIdLookup;
+ private String[] mTableNames;
+
+ @NonNull
+ @VisibleForTesting
+ long[] mTableVersions;
+
+ private Object[] mQueryArgs = new Object[1];
+
+ // max id in the last syc
+ private long mMaxVersion = 0;
+
+ private final RoomDatabase mDatabase;
+
+ AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
+
+ private volatile boolean mInitialized = false;
+
+ private volatile SupportSQLiteStatement mCleanupStatement;
+
+ private ObservedTableTracker mObservedTableTracker;
+
+ // should be accessed with synchronization only.
+ @VisibleForTesting
+ final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
+
+ /**
+ * Used by the generated code.
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public InvalidationTracker(RoomDatabase database, String... tableNames) {
+ mDatabase = database;
+ mObservedTableTracker = new ObservedTableTracker(tableNames.length);
+ mTableIdLookup = new ArrayMap<>();
+ final int size = tableNames.length;
+ mTableNames = new String[size];
+ for (int id = 0; id < size; id++) {
+ final String tableName = tableNames[id].toLowerCase(Locale.US);
+ mTableIdLookup.put(tableName, id);
+ mTableNames[id] = tableName;
+ }
+ mTableVersions = new long[tableNames.length];
+ Arrays.fill(mTableVersions, 0);
+ }
+
+ /**
+ * Internal method to initialize table tracking.
+ * <p>
+ * You should never call this method, it is called by the generated code.
+ */
+ void internalInit(SupportSQLiteDatabase database) {
+ synchronized (this) {
+ if (mInitialized) {
+ Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
+ return;
+ }
+
+ database.beginTransaction();
+ try {
+ database.execSQL("PRAGMA temp_store = MEMORY;");
+ database.execSQL("PRAGMA recursive_triggers='ON';");
+ database.execSQL(CREATE_VERSION_TABLE_SQL);
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ syncTriggers(database);
+ mCleanupStatement = database.compileStatement(CLEANUP_SQL);
+ mInitialized = true;
+ }
+ }
+
+ private static void appendTriggerName(StringBuilder builder, String tableName,
+ String triggerType) {
+ builder.append("`")
+ .append("room_table_modification_trigger_")
+ .append(tableName)
+ .append("_")
+ .append(triggerType)
+ .append("`");
+ }
+
+ private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
+ final String tableName = mTableNames[tableId];
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String trigger : TRIGGERS) {
+ stringBuilder.setLength(0);
+ stringBuilder.append("DROP TRIGGER IF EXISTS ");
+ appendTriggerName(stringBuilder, tableName, trigger);
+ writableDb.execSQL(stringBuilder.toString());
+ }
+ }
+
+ private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
+ final String tableName = mTableNames[tableId];
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String trigger : TRIGGERS) {
+ stringBuilder.setLength(0);
+ stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
+ appendTriggerName(stringBuilder, tableName, trigger);
+ stringBuilder.append(" AFTER ")
+ .append(trigger)
+ .append(" ON `")
+ .append(tableName)
+ .append("` BEGIN INSERT OR REPLACE INTO ")
+ .append(UPDATE_TABLE_NAME)
+ .append(" VALUES(null, ")
+ .append(tableId)
+ .append("); END");
+ writableDb.execSQL(stringBuilder.toString());
+ }
+ }
+
+ /**
+ * Adds the given observer to the observers list and it will be notified if any table it
+ * observes changes.
+ * <p>
+ * Database changes are pulled on another thread so in some race conditions, the observer might
+ * be invoked for changes that were done before it is added.
+ * <p>
+ * If the observer already exists, this is a no-op call.
+ * <p>
+ * If one of the tables in the Observer does not exist in the database, this method throws an
+ * {@link IllegalArgumentException}.
+ *
+ * @param observer The observer which listens the database for changes.
+ */
+ @WorkerThread
+ public void addObserver(@NonNull Observer observer) {
+ final String[] tableNames = observer.mTables;
+ int[] tableIds = new int[tableNames.length];
+ final int size = tableNames.length;
+ long[] versions = new long[tableNames.length];
+
+ // TODO sync versions ?
+ for (int i = 0; i < size; i++) {
+ Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
+ if (tableId == null) {
+ throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
+ }
+ tableIds[i] = tableId;
+ versions[i] = mMaxVersion;
+ }
+ ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames, versions);
+ ObserverWrapper currentObserver;
+ synchronized (mObserverMap) {
+ currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
+ }
+ if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
+ syncTriggers();
+ }
+ }
+
+ /**
+ * Adds an observer but keeps a weak reference back to it.
+ * <p>
+ * Note that you cannot remove this observer once added. It will be automatically removed
+ * when the observer is GC'ed.
+ *
+ * @param observer The observer to which InvalidationTracker will keep a weak reference.
+ * @hide
+ */
+ @SuppressWarnings("unused")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public void addWeakObserver(Observer observer) {
+ addObserver(new WeakObserver(this, observer));
+ }
+
+ /**
+ * Removes the observer from the observers list.
+ *
+ * @param observer The observer to remove.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @WorkerThread
+ public void removeObserver(@NonNull final Observer observer) {
+ ObserverWrapper wrapper;
+ synchronized (mObserverMap) {
+ wrapper = mObserverMap.remove(observer);
+ }
+ if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
+ syncTriggers();
+ }
+ }
+
+ private boolean ensureInitialization() {
+ if (!mDatabase.isOpen()) {
+ return false;
+ }
+ if (!mInitialized) {
+ // trigger initialization
+ mDatabase.getOpenHelper().getWritableDatabase();
+ }
+ if (!mInitialized) {
+ Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ Runnable mRefreshRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final Lock closeLock = mDatabase.getCloseLock();
+ boolean hasUpdatedTable = false;
+ try {
+ closeLock.lock();
+
+ if (!ensureInitialization()) {
+ return;
+ }
+
+ 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;
+ if (mDatabase.mWriteAheadLoggingEnabled) {
+ // This transaction has to be on the underlying DB rather than the RoomDatabase
+ // in order to avoid a recursive loop after endTransaction.
+ SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
+ try {
+ db.beginTransaction();
+ hasUpdatedTable = checkUpdatedTable();
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ } else {
+ hasUpdatedTable = checkUpdatedTable();
+ }
+ } catch (IllegalStateException | SQLiteException exception) {
+ // may happen if db is closed. just log.
+ Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+ exception);
+ } finally {
+ closeLock.unlock();
+ }
+ if (hasUpdatedTable) {
+ synchronized (mObserverMap) {
+ for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
+ entry.getValue().checkForInvalidation(mTableVersions);
+ }
+ }
+ }
+ }
+
+ private boolean checkUpdatedTable() {
+ boolean hasUpdatedTable = false;
+ Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs);
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ while (cursor.moveToNext()) {
+ final long version = cursor.getLong(0);
+ final int tableId = cursor.getInt(1);
+
+ mTableVersions[tableId] = version;
+ hasUpdatedTable = true;
+ // result is ordered so we can safely do this assignment
+ mMaxVersion = version;
+ }
+ } finally {
+ cursor.close();
+ }
+ return hasUpdatedTable;
+ }
+ };
+
+ /**
+ * Enqueues a task to refresh the list of updated tables.
+ * <p>
+ * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
+ * if you have another connection to the database or directly use {@link
+ * SupportSQLiteDatabase}, you may need to call this manually.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void refreshVersionsAsync() {
+ // TODO we should consider doing this sync instead of async.
+ if (mPendingRefresh.compareAndSet(false, true)) {
+ ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
+ }
+ }
+
+ /**
+ * Check versions for tables, and run observers synchronously if tables have been updated.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @WorkerThread
+ public void refreshVersionsSync() {
+ syncTriggers();
+ mRefreshRunnable.run();
+ }
+
+ void syncTriggers(SupportSQLiteDatabase database) {
+ if (database.inTransaction()) {
+ // we won't run this inside another transaction.
+ return;
+ }
+ try {
+ // This method runs in a while loop because while changes are synced to db, another
+ // runnable may be skipped. If we cause it to skip, we need to do its work.
+ while (true) {
+ Lock closeLock = mDatabase.getCloseLock();
+ closeLock.lock();
+ try {
+ // there is a potential race condition where another mSyncTriggers runnable
+ // can start running right after we get the tables list to sync.
+ final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
+ if (tablesToSync == null) {
+ return;
+ }
+ final int limit = tablesToSync.length;
+ try {
+ database.beginTransaction();
+ for (int tableId = 0; tableId < limit; tableId++) {
+ switch (tablesToSync[tableId]) {
+ case ObservedTableTracker.ADD:
+ startTrackingTable(database, tableId);
+ break;
+ case ObservedTableTracker.REMOVE:
+ stopTrackingTable(database, tableId);
+ break;
+ }
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ mObservedTableTracker.onSyncCompleted();
+ } finally {
+ closeLock.unlock();
+ }
+ }
+ } catch (IllegalStateException | SQLiteException exception) {
+ // may happen if db is closed. just log.
+ Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+ exception);
+ }
+ }
+
+ /**
+ * Called by RoomDatabase before each beginTransaction call.
+ * <p>
+ * It is important that pending trigger changes are applied to the database before any query
+ * runs. Otherwise, we may miss some changes.
+ * <p>
+ * This api should eventually be public.
+ */
+ void syncTriggers() {
+ if (!mDatabase.isOpen()) {
+ return;
+ }
+ syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
+ }
+
+ /**
+ * Wraps an observer and keeps the table information.
+ * <p>
+ * Internally table ids are used which may change from database to database so the table
+ * related information is kept here rather than in the Observer.
+ */
+ @SuppressWarnings("WeakerAccess")
+ static class ObserverWrapper {
+ final int[] mTableIds;
+ private final String[] mTableNames;
+ private final long[] mVersions;
+ final Observer mObserver;
+ private final Set<String> mSingleTableSet;
+
+ ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames, long[] versions) {
+ mObserver = observer;
+ mTableIds = tableIds;
+ mTableNames = tableNames;
+ mVersions = versions;
+ if (tableIds.length == 1) {
+ ArraySet<String> set = new ArraySet<>();
+ set.add(mTableNames[0]);
+ mSingleTableSet = Collections.unmodifiableSet(set);
+ } else {
+ mSingleTableSet = null;
+ }
+ }
+
+ void checkForInvalidation(long[] versions) {
+ Set<String> invalidatedTables = null;
+ final int size = mTableIds.length;
+ for (int index = 0; index < size; index++) {
+ final int tableId = mTableIds[index];
+ final long newVersion = versions[tableId];
+ final long currentVersion = mVersions[index];
+ if (currentVersion < newVersion) {
+ mVersions[index] = newVersion;
+ if (size == 1) {
+ // Optimization for a single-table observer
+ invalidatedTables = mSingleTableSet;
+ } else {
+ if (invalidatedTables == null) {
+ invalidatedTables = new ArraySet<>(size);
+ }
+ invalidatedTables.add(mTableNames[index]);
+ }
+ }
+ }
+ if (invalidatedTables != null) {
+ mObserver.onInvalidated(invalidatedTables);
+ }
+ }
+ }
+
+ /**
+ * An observer that can listen for changes in the database.
+ */
+ public abstract static class Observer {
+ final String[] mTables;
+
+ /**
+ * Observes the given list of tables.
+ *
+ * @param firstTable The table name
+ * @param rest More table names
+ */
+ @SuppressWarnings("unused")
+ protected Observer(@NonNull String firstTable, String... rest) {
+ mTables = Arrays.copyOf(rest, rest.length + 1);
+ mTables[rest.length] = firstTable;
+ }
+
+ /**
+ * Observes the given list of tables.
+ *
+ * @param tables The list of tables to observe for changes.
+ */
+ public Observer(@NonNull String[] tables) {
+ // copy tables in case user modifies them afterwards
+ mTables = Arrays.copyOf(tables, tables.length);
+ }
+
+ /**
+ * Called when one of the observed tables is invalidated in the database.
+ *
+ * @param tables A set of invalidated tables. This is useful when the observer targets
+ * multiple tables and want to know which table is invalidated.
+ */
+ public abstract void onInvalidated(@NonNull Set<String> tables);
+ }
+
+
+ /**
+ * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
+ * triggers in the database.
+ * <p>
+ * This class is thread safe
+ */
+ static class ObservedTableTracker {
+ static final int NO_OP = 0; // don't change trigger state for this table
+ static final int ADD = 1; // add triggers for this table
+ static final int REMOVE = 2; // remove triggers for this table
+
+ // number of observers per table
+ final long[] mTableObservers;
+ // trigger state for each table at last sync
+ // this field is updated when syncAndGet is called.
+ final boolean[] mTriggerStates;
+ // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
+ final int[] mTriggerStateChanges;
+
+ boolean mNeedsSync;
+
+ /**
+ * After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
+ * returning any non-null value from getTablesToSync.
+ * This allows us to workaround any multi-threaded state syncing issues.
+ */
+ boolean mPendingSync;
+
+ ObservedTableTracker(int tableCount) {
+ mTableObservers = new long[tableCount];
+ mTriggerStates = new boolean[tableCount];
+ mTriggerStateChanges = new int[tableCount];
+ Arrays.fill(mTableObservers, 0);
+ Arrays.fill(mTriggerStates, false);
+ }
+
+ /**
+ * @return true if # of triggers is affected.
+ */
+ boolean onAdded(int... tableIds) {
+ boolean needTriggerSync = false;
+ synchronized (this) {
+ for (int tableId : tableIds) {
+ final long prevObserverCount = mTableObservers[tableId];
+ mTableObservers[tableId] = prevObserverCount + 1;
+ if (prevObserverCount == 0) {
+ mNeedsSync = true;
+ needTriggerSync = true;
+ }
+ }
+ }
+ return needTriggerSync;
+ }
+
+ /**
+ * @return true if # of triggers is affected.
+ */
+ boolean onRemoved(int... tableIds) {
+ boolean needTriggerSync = false;
+ synchronized (this) {
+ for (int tableId : tableIds) {
+ final long prevObserverCount = mTableObservers[tableId];
+ mTableObservers[tableId] = prevObserverCount - 1;
+ if (prevObserverCount == 1) {
+ mNeedsSync = true;
+ needTriggerSync = true;
+ }
+ }
+ }
+ return needTriggerSync;
+ }
+
+ /**
+ * If this returns non-null, you must call onSyncCompleted.
+ *
+ * @return int[] An int array where the index for each tableId has the action for that
+ * table.
+ */
+ @Nullable
+ int[] getTablesToSync() {
+ synchronized (this) {
+ if (!mNeedsSync || mPendingSync) {
+ return null;
+ }
+ final int tableCount = mTableObservers.length;
+ for (int i = 0; i < tableCount; i++) {
+ final boolean newState = mTableObservers[i] > 0;
+ if (newState != mTriggerStates[i]) {
+ mTriggerStateChanges[i] = newState ? ADD : REMOVE;
+ } else {
+ mTriggerStateChanges[i] = NO_OP;
+ }
+ mTriggerStates[i] = newState;
+ }
+ mPendingSync = true;
+ mNeedsSync = false;
+ return mTriggerStateChanges;
+ }
+ }
+
+ /**
+ * if getTablesToSync returned non-null, the called should call onSyncCompleted once it
+ * is done.
+ */
+ void onSyncCompleted() {
+ synchronized (this) {
+ mPendingSync = false;
+ }
+ }
+ }
+
+ /**
+ * An Observer wrapper that keeps a weak reference to the given object.
+ * <p>
+ * This class with automatically unsubscribe when the wrapped observer goes out of memory.
+ */
+ static class WeakObserver extends Observer {
+ final InvalidationTracker mTracker;
+ final WeakReference<Observer> mDelegateRef;
+
+ WeakObserver(InvalidationTracker tracker, Observer delegate) {
+ super(delegate.mTables);
+ mTracker = tracker;
+ mDelegateRef = new WeakReference<>(delegate);
+ }
+
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ final Observer observer = mDelegateRef.get();
+ if (observer == null) {
+ mTracker.removeObserver(this);
+ } else {
+ observer.onInvalidated(tables);
+ }
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/Room.java b/room/runtime/src/main/java/androidx/room/Room.java
new file mode 100644
index 0000000..15107fc
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/Room.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility class for Room.
+ */
+@SuppressWarnings("unused")
+public class Room {
+ static final String LOG_TAG = "ROOM";
+ /**
+ * The master table where room keeps its metadata information.
+ */
+ public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
+ private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
+
+ /**
+ * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
+ * should keep a reference to it and re-use it.
+ *
+ * @param context The context for the database. This is usually the Application context.
+ * @param klass The abstract class which is annotated with {@link Database} and extends
+ * {@link RoomDatabase}.
+ * @param name The name of the database file.
+ * @param <T> The type of the database class.
+ * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @NonNull
+ public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder(
+ @NonNull Context context, @NonNull Class<T> klass, @NonNull String name) {
+ //noinspection ConstantConditions
+ if (name == null || name.trim().length() == 0) {
+ throw new IllegalArgumentException("Cannot build a database with null or empty name."
+ + " If you are trying to create an in memory database, use Room"
+ + ".inMemoryDatabaseBuilder");
+ }
+ return new RoomDatabase.Builder<>(context, klass, name);
+ }
+
+ /**
+ * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
+ * database disappears when the process is killed.
+ * Once a database is built, you should keep a reference to it and re-use it.
+ *
+ * @param context The context for the database. This is usually the Application context.
+ * @param klass The abstract class which is annotated with {@link Database} and extends
+ * {@link RoomDatabase}.
+ * @param <T> The type of the database class.
+ * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
+ */
+ @NonNull
+ public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder(
+ @NonNull Context context, @NonNull Class<T> klass) {
+ return new RoomDatabase.Builder<>(context, klass, null);
+ }
+
+ @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
+ @NonNull
+ static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {
+ final String fullPackage = klass.getPackage().getName();
+ String name = klass.getCanonicalName();
+ final String postPackageName = fullPackage.isEmpty()
+ ? name
+ : (name.substring(fullPackage.length() + 1));
+ final String implName = postPackageName.replace('.', '_') + suffix;
+ //noinspection TryWithIdenticalCatches
+ try {
+
+ @SuppressWarnings("unchecked")
+ final Class<T> aClass = (Class<T>) Class.forName(
+ fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
+ return aClass.newInstance();
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("cannot find implementation for "
+ + klass.getCanonicalName() + ". " + implName + " does not exist");
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot access the constructor"
+ + klass.getCanonicalName());
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Failed to create an instance of "
+ + klass.getCanonicalName());
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
new file mode 100644
index 0000000..d89cf00
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -0,0 +1,758 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+import androidx.collection.SparseArrayCompat;
+import androidx.core.app.ActivityManagerCompat;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Base class for all Room databases. All classes that are annotated with {@link Database} must
+ * extend this class.
+ * <p>
+ * RoomDatabase provides direct access to the underlying database implementation but you should
+ * prefer using {@link Dao} classes.
+ *
+ * @see Database
+ */
+//@SuppressWarnings({"unused", "WeakerAccess"})
+public abstract class RoomDatabase {
+ private static final String DB_IMPL_SUFFIX = "_Impl";
+ /**
+ * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int MAX_BIND_PARAMETER_CNT = 999;
+ // set by the generated open helper.
+ protected volatile SupportSQLiteDatabase mDatabase;
+ private SupportSQLiteOpenHelper mOpenHelper;
+ private final InvalidationTracker mInvalidationTracker;
+ private boolean mAllowMainThreadQueries;
+ boolean mWriteAheadLoggingEnabled;
+
+ @Nullable
+ protected List<Callback> mCallbacks;
+
+ private final ReentrantLock mCloseLock = new ReentrantLock();
+
+ /**
+ * {@link InvalidationTracker} uses this lock to prevent the database from closing while it is
+ * querying database updates.
+ *
+ * @return The lock for {@link #close()}.
+ */
+ Lock getCloseLock() {
+ return mCloseLock;
+ }
+
+ /**
+ * Creates a RoomDatabase.
+ * <p>
+ * You cannot create an instance of a database, instead, you should acquire it via
+ * {@link Room#databaseBuilder(Context, Class, String)} or
+ * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
+ */
+ public RoomDatabase() {
+ mInvalidationTracker = createInvalidationTracker();
+ }
+
+ /**
+ * Called by {@link Room} when it is initialized.
+ *
+ * @param configuration The database configuration.
+ */
+ @CallSuper
+ public void init(@NonNull DatabaseConfiguration configuration) {
+ mOpenHelper = createOpenHelper(configuration);
+ boolean wal = false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING;
+ mOpenHelper.setWriteAheadLoggingEnabled(wal);
+ }
+ mCallbacks = configuration.callbacks;
+ mAllowMainThreadQueries = configuration.allowMainThreadQueries;
+ mWriteAheadLoggingEnabled = wal;
+ }
+
+ /**
+ * Returns the SQLite open helper used by this database.
+ *
+ * @return The SQLite open helper used by this database.
+ */
+ @NonNull
+ public SupportSQLiteOpenHelper getOpenHelper() {
+ return mOpenHelper;
+ }
+
+ /**
+ * Creates the open helper to access the database. Generated class already implements this
+ * method.
+ * Note that this method is called when the RoomDatabase is initialized.
+ *
+ * @param config The configuration of the Room database.
+ * @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
+ */
+ @NonNull
+ protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
+
+ /**
+ * Called when the RoomDatabase is created.
+ * <p>
+ * This is already implemented by the generated code.
+ *
+ * @return Creates a new InvalidationTracker.
+ */
+ @NonNull
+ protected abstract InvalidationTracker createInvalidationTracker();
+
+ /**
+ * Deletes all rows from all the tables that are registered to this database as
+ * {@link Database#entities()}.
+ * <p>
+ * This does NOT reset the auto-increment value generated by {@link PrimaryKey#autoGenerate()}.
+ * <p>
+ * After deleting the rows, Room will set a WAL checkpoint and run VACUUM. This means that the
+ * data is completely erased. The space will be reclaimed by the system if the amount surpasses
+ * the threshold of database file size.
+ *
+ * @see <a href="https://www.sqlite.org/fileformat.html">Database File Format</a>
+ */
+ @WorkerThread
+ public abstract void clearAllTables();
+
+ /**
+ * Returns true if database connection is open and initialized.
+ *
+ * @return true if the database connection is open, false otherwise.
+ */
+ public boolean isOpen() {
+ final SupportSQLiteDatabase db = mDatabase;
+ return db != null && db.isOpen();
+ }
+
+ /**
+ * Closes the database if it is already open.
+ */
+ public void close() {
+ if (isOpen()) {
+ try {
+ mCloseLock.lock();
+ mOpenHelper.close();
+ } finally {
+ mCloseLock.unlock();
+ }
+ }
+ }
+
+ /**
+ * Asserts that we are not on the main thread.
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // used in generated code
+ public void assertNotMainThread() {
+ if (mAllowMainThreadQueries) {
+ return;
+ }
+ 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.");
+ }
+ }
+
+ // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
+ // methods we are using and also helps unit tests to mock this class without mocking
+ // all SQLite database methods.
+
+ /**
+ * Convenience method to query the database with arguments.
+ *
+ * @param query The sql query
+ * @param args The bind arguments for the placeholders in the query
+ *
+ * @return A Cursor obtained by running the given query in the Room database.
+ */
+ public Cursor query(String query, @Nullable Object[] args) {
+ return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
+ *
+ * @param query The Query which includes the SQL and a bind callback for bind arguments.
+ * @return Result of the query.
+ */
+ public Cursor query(SupportSQLiteQuery query) {
+ assertNotMainThread();
+ return mOpenHelper.getWritableDatabase().query(query);
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#compileStatement(String)}.
+ *
+ * @param sql The query to compile.
+ * @return The compiled query.
+ */
+ public SupportSQLiteStatement compileStatement(@NonNull String sql) {
+ assertNotMainThread();
+ return mOpenHelper.getWritableDatabase().compileStatement(sql);
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#beginTransaction()}.
+ */
+ public void beginTransaction() {
+ assertNotMainThread();
+ SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
+ mInvalidationTracker.syncTriggers(database);
+ database.beginTransaction();
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}.
+ */
+ public void endTransaction() {
+ mOpenHelper.getWritableDatabase().endTransaction();
+ if (!inTransaction()) {
+ // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last
+ // endTransaction call to do it.
+ mInvalidationTracker.refreshVersionsAsync();
+ }
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
+ */
+ public void setTransactionSuccessful() {
+ mOpenHelper.getWritableDatabase().setTransactionSuccessful();
+ }
+
+ /**
+ * Executes the specified {@link Runnable} in a database transaction. The transaction will be
+ * marked as successful unless an exception is thrown in the {@link Runnable}.
+ *
+ * @param body The piece of code to execute.
+ */
+ public void runInTransaction(@NonNull Runnable body) {
+ beginTransaction();
+ try {
+ body.run();
+ setTransactionSuccessful();
+ } finally {
+ endTransaction();
+ }
+ }
+
+ /**
+ * Executes the specified {@link Callable} in a database transaction. The transaction will be
+ * marked as successful unless an exception is thrown in the {@link Callable}.
+ *
+ * @param body The piece of code to execute.
+ * @param <V> The type of the return value.
+ * @return The value returned from the {@link Callable}.
+ */
+ public <V> V runInTransaction(@NonNull Callable<V> body) {
+ beginTransaction();
+ try {
+ V result = body.call();
+ setTransactionSuccessful();
+ return result;
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException("Exception in transaction", e);
+ } finally {
+ endTransaction();
+ }
+ }
+
+ /**
+ * Called by the generated code when database is open.
+ * <p>
+ * You should never call this method manually.
+ *
+ * @param db The database instance.
+ */
+ protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
+ mInvalidationTracker.internalInit(db);
+ }
+
+ /**
+ * Returns the invalidation tracker for this database.
+ * <p>
+ * You can use the invalidation tracker to get notified when certain tables in the database
+ * are modified.
+ *
+ * @return The invalidation tracker for the database.
+ */
+ @NonNull
+ public InvalidationTracker getInvalidationTracker() {
+ return mInvalidationTracker;
+ }
+
+ /**
+ * Returns true if current thread is in a transaction.
+ *
+ * @return True if there is an active transaction in current thread, false otherwise.
+ * @see SupportSQLiteDatabase#inTransaction()
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean inTransaction() {
+ return mOpenHelper.getWritableDatabase().inTransaction();
+ }
+
+ /**
+ * Journal modes for SQLite database.
+ *
+ * @see RoomDatabase.Builder#setJournalMode(JournalMode)
+ */
+ public enum JournalMode {
+
+ /**
+ * Let Room choose the journal mode. This is the default value when no explicit value is
+ * specified.
+ * <p>
+ * The actual value will be {@link #TRUNCATE} when the device runs API Level lower than 16
+ * or it is a low-RAM device. Otherwise, {@link #WRITE_AHEAD_LOGGING} will be used.
+ */
+ AUTOMATIC,
+
+ /**
+ * Truncate journal mode.
+ */
+ TRUNCATE,
+
+ /**
+ * Write-Ahead Logging mode.
+ */
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+ WRITE_AHEAD_LOGGING;
+
+ /**
+ * Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or
+ * {@link #WRITE_AHEAD_LOGGING}.
+ */
+ @SuppressLint("NewApi")
+ JournalMode resolve(Context context) {
+ if (this != AUTOMATIC) {
+ return this;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ ActivityManager manager = (ActivityManager)
+ context.getSystemService(Context.ACTIVITY_SERVICE);
+ if (manager != null && !ActivityManagerCompat.isLowRamDevice(manager)) {
+ return WRITE_AHEAD_LOGGING;
+ }
+ }
+ return TRUNCATE;
+ }
+ }
+
+ /**
+ * Builder for RoomDatabase.
+ *
+ * @param <T> The type of the abstract database class.
+ */
+ public static class Builder<T extends RoomDatabase> {
+ private final Class<T> mDatabaseClass;
+ private final String mName;
+ private final Context mContext;
+ private ArrayList<Callback> mCallbacks;
+
+ private SupportSQLiteOpenHelper.Factory mFactory;
+ private boolean mAllowMainThreadQueries;
+ private JournalMode mJournalMode;
+ private boolean mRequireMigration;
+ /**
+ * Migrations, mapped by from-to pairs.
+ */
+ private final MigrationContainer mMigrationContainer;
+ private Set<Integer> mMigrationsNotRequiredFrom;
+ /**
+ * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
+ * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
+ * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(int...)}.
+ */
+ private Set<Integer> mMigrationStartAndEndVersions;
+
+ Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
+ mContext = context;
+ mDatabaseClass = klass;
+ mName = name;
+ mJournalMode = JournalMode.AUTOMATIC;
+ mRequireMigration = true;
+ mMigrationContainer = new MigrationContainer();
+ }
+
+ /**
+ * Sets the database factory. If not set, it defaults to
+ * {@link FrameworkSQLiteOpenHelperFactory}.
+ *
+ * @param factory The factory to use to access the database.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) {
+ mFactory = factory;
+ return this;
+ }
+
+ /**
+ * Adds a migration to the builder.
+ * <p>
+ * Each Migration has a start and end versions and Room runs these migrations to bring the
+ * database to the latest version.
+ * <p>
+ * If a migration item is missing between current version and the latest version, Room
+ * will clear the database and recreate so even if you have no changes between 2 versions,
+ * you should still provide a Migration object to the builder.
+ * <p>
+ * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+ * going version 3 to 5 without going to version 4). If Room opens a database at version
+ * 3 and latest version is >= 5, Room will use the migration object that can migrate from
+ * 3 to 5 instead of 3 to 4 and 4 to 5.
+ *
+ * @param migrations The migration object that can modify the database and to the necessary
+ * changes.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ if (mMigrationStartAndEndVersions == null) {
+ mMigrationStartAndEndVersions = new HashSet<>();
+ }
+ for (Migration migration: migrations) {
+ mMigrationStartAndEndVersions.add(migration.startVersion);
+ mMigrationStartAndEndVersions.add(migration.endVersion);
+ }
+
+ mMigrationContainer.addMigrations(migrations);
+ return this;
+ }
+
+ /**
+ * Disables the main thread query check for Room.
+ * <p>
+ * Room ensures that Database is never accessed on the main thread because it may lock the
+ * main thread and trigger an ANR. If you need to access the database from the main thread,
+ * you should always use async alternatives or manually move the call to a background
+ * thread.
+ * <p>
+ * You may want to turn this check off for testing.
+ *
+ * @return this
+ */
+ @NonNull
+ public Builder<T> allowMainThreadQueries() {
+ mAllowMainThreadQueries = true;
+ return this;
+ }
+
+ /**
+ * Sets the journal mode for this database.
+ *
+ * <p>
+ * This value is ignored if the builder is initialized with
+ * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
+ * <p>
+ * The journal mode should be consistent across multiple instances of
+ * {@link RoomDatabase} for a single SQLite database file.
+ * <p>
+ * The default value is {@link JournalMode#AUTOMATIC}.
+ *
+ * @param journalMode The journal mode.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> setJournalMode(@NonNull JournalMode journalMode) {
+ mJournalMode = journalMode;
+ return this;
+ }
+
+ /**
+ * 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>
+ * If it cannot find the set of {@link Migration}s that will bring the database to the
+ * current version, it will throw an {@link IllegalStateException}.
+ * <p>
+ * You can call this method to change this behavior to re-create the database instead of
+ * crashing.
+ * <p>
+ * Note that this will delete all of the data in the database tables managed by Room.
+ *
+ * @return this
+ */
+ @NonNull
+ public Builder<T> fallbackToDestructiveMigration() {
+ mRequireMigration = false;
+ return this;
+ }
+
+ /**
+ * Informs Room that it is allowed to destructively recreate database tables from specific
+ * starting schema versions.
+ * <p>
+ * This functionality is the same as that provided by
+ * {@link #fallbackToDestructiveMigration()}, except that this method allows the
+ * specification of a set of schema versions for which destructive recreation is allowed.
+ * <p>
+ * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
+ * to allow destructive migrations from some schema versions while still taking advantage
+ * of exceptions being thrown due to unintentionally missing migrations.
+ * <p>
+ * Note: No versions passed to this method may also exist as either starting or ending
+ * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
+ * version passed to this method is found as a starting or ending version in a Migration, an
+ * exception will be thrown.
+ *
+ * @param startVersions The set of schema versions from which Room should use a destructive
+ * migration.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> fallbackToDestructiveMigrationFrom(int... startVersions) {
+ if (mMigrationsNotRequiredFrom == null) {
+ mMigrationsNotRequiredFrom = new HashSet<>(startVersions.length);
+ }
+ for (int startVersion : startVersions) {
+ mMigrationsNotRequiredFrom.add(startVersion);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a {@link Callback} to this database.
+ *
+ * @param callback The callback.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> addCallback(@NonNull Callback callback) {
+ if (mCallbacks == null) {
+ mCallbacks = new ArrayList<>();
+ }
+ mCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Creates the databases and initializes it.
+ * <p>
+ * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
+ * triggers.
+ *
+ * @return A new database instance.
+ */
+ @NonNull
+ public T build() {
+ //noinspection ConstantConditions
+ if (mContext == null) {
+ throw new IllegalArgumentException("Cannot provide null context for the database.");
+ }
+ //noinspection ConstantConditions
+ if (mDatabaseClass == null) {
+ throw new IllegalArgumentException("Must provide an abstract class that"
+ + " extends RoomDatabase");
+ }
+
+ if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
+ for (Integer version : mMigrationStartAndEndVersions) {
+ if (mMigrationsNotRequiredFrom.contains(version)) {
+ throw new IllegalArgumentException(
+ "Inconsistency detected. A Migration was supplied to "
+ + "addMigration(Migration... migrations) that has a start "
+ + "or end version equal to a start version supplied to "
+ + "fallbackToDestructiveMigrationFrom(int... "
+ + "startVersions). Start version: "
+ + version);
+ }
+ }
+ }
+
+ if (mFactory == null) {
+ mFactory = new FrameworkSQLiteOpenHelperFactory();
+ }
+ DatabaseConfiguration configuration =
+ new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
+ mCallbacks, mAllowMainThreadQueries,
+ mJournalMode.resolve(mContext),
+ mRequireMigration, mMigrationsNotRequiredFrom);
+ T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
+ db.init(configuration);
+ return db;
+ }
+ }
+
+ /**
+ * A container to hold migrations. It also allows querying its contents to find migrations
+ * between two versions.
+ */
+ public static class MigrationContainer {
+ private SparseArrayCompat<SparseArrayCompat<Migration>> mMigrations =
+ new SparseArrayCompat<>();
+
+ /**
+ * Adds the given migrations to the list of available migrations. If 2 migrations have the
+ * same start-end versions, the latter migration overrides the previous one.
+ *
+ * @param migrations List of available migrations.
+ */
+ public void addMigrations(@NonNull Migration... migrations) {
+ for (Migration migration : migrations) {
+ addMigration(migration);
+ }
+ }
+
+ private void addMigration(Migration migration) {
+ final int start = migration.startVersion;
+ final int end = migration.endVersion;
+ SparseArrayCompat<Migration> targetMap = mMigrations.get(start);
+ if (targetMap == null) {
+ targetMap = new SparseArrayCompat<>();
+ mMigrations.put(start, targetMap);
+ }
+ Migration existing = targetMap.get(end);
+ if (existing != null) {
+ Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
+ }
+ targetMap.append(end, migration);
+ }
+
+ /**
+ * Finds the list of migrations that should be run to move from {@code start} version to
+ * {@code end} version.
+ *
+ * @param start The current database version
+ * @param end The target database version
+ * @return An ordered list of {@link Migration} objects that should be run to migrate
+ * between the given versions. If a migration path cannot be found, returns {@code null}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public List<Migration> findMigrationPath(int start, int end) {
+ if (start == end) {
+ return Collections.emptyList();
+ }
+ boolean migrateUp = end > start;
+ List<Migration> result = new ArrayList<>();
+ return findUpMigrationPath(result, migrateUp, start, end);
+ }
+
+ private List<Migration> findUpMigrationPath(List<Migration> result, boolean upgrade,
+ int start, int end) {
+ final int searchDirection = upgrade ? -1 : 1;
+ while (upgrade ? start < end : start > end) {
+ SparseArrayCompat<Migration> targetNodes = mMigrations.get(start);
+ if (targetNodes == null) {
+ return null;
+ }
+ // keys are ordered so we can start searching from one end of them.
+ final int size = targetNodes.size();
+ final int firstIndex;
+ final int lastIndex;
+
+ if (upgrade) {
+ firstIndex = size - 1;
+ lastIndex = -1;
+ } else {
+ firstIndex = 0;
+ lastIndex = size;
+ }
+ boolean found = false;
+ for (int i = firstIndex; i != lastIndex; i += searchDirection) {
+ final int targetVersion = targetNodes.keyAt(i);
+ final boolean shouldAddToPath;
+ if (upgrade) {
+ shouldAddToPath = targetVersion <= end && targetVersion > start;
+ } else {
+ shouldAddToPath = targetVersion >= end && targetVersion < start;
+ }
+ if (shouldAddToPath) {
+ result.add(targetNodes.valueAt(i));
+ start = targetVersion;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return null;
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Callback for {@link RoomDatabase}.
+ */
+ public abstract static class Callback {
+
+ /**
+ * Called when the database is created for the first time. This is called after all the
+ * tables are created.
+ *
+ * @param db The database.
+ */
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ }
+
+ /**
+ * Called when the database has been opened.
+ *
+ * @param db The database.
+ */
+ public void onOpen(@NonNull SupportSQLiteDatabase db) {
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/RoomOpenHelper.java b/room/runtime/src/main/java/androidx/room/RoomOpenHelper.java
new file mode 100644
index 0000000..d42a95f
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/RoomOpenHelper.java
@@ -0,0 +1,184 @@
+/*
+ * 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 androidx.room;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.List;
+
+/**
+ * An open helper that holds a reference to the configuration until the database is opened.
+ *
+ * @hide
+ */
+@SuppressWarnings("unused")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
+ @Nullable
+ private DatabaseConfiguration mConfiguration;
+ @NonNull
+ private final Delegate mDelegate;
+ @NonNull
+ private final String mIdentityHash;
+ /**
+ * Room v1 had a bug where the hash was not consistent if fields are reordered.
+ * The new has fixes it but we still need to accept the legacy hash.
+ */
+ @NonNull // b/64290754
+ private final String mLegacyHash;
+
+ public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
+ @NonNull String identityHash, @NonNull String legacyHash) {
+ super(delegate.version);
+ mConfiguration = configuration;
+ mDelegate = delegate;
+ mIdentityHash = identityHash;
+ mLegacyHash = legacyHash;
+ }
+
+ @Override
+ public void onConfigure(SupportSQLiteDatabase db) {
+ super.onConfigure(db);
+ }
+
+ @Override
+ public void onCreate(SupportSQLiteDatabase db) {
+ updateIdentity(db);
+ mDelegate.createAllTables(db);
+ mDelegate.onCreate(db);
+ }
+
+ @Override
+ public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ boolean migrated = false;
+ if (mConfiguration != null) {
+ List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
+ oldVersion, newVersion);
+ if (migrations != null) {
+ for (Migration migration : migrations) {
+ migration.migrate(db);
+ }
+ mDelegate.validateMigration(db);
+ updateIdentity(db);
+ migrated = true;
+ }
+ }
+ if (!migrated) {
+ if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) {
+ mDelegate.dropAllTables(db);
+ mDelegate.createAllTables(db);
+ } else {
+ throw new IllegalStateException("A migration from " + oldVersion + " to "
+ + newVersion + " was required but not found. Please provide the "
+ + "necessary Migration path via "
+ + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ + "destructive migrations via one of the "
+ + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
+ }
+ }
+ }
+
+ @Override
+ public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ onUpgrade(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onOpen(SupportSQLiteDatabase db) {
+ super.onOpen(db);
+ checkIdentity(db);
+ mDelegate.onOpen(db);
+ // there might be too many configurations etc, just clear it.
+ mConfiguration = null;
+ }
+
+ private void checkIdentity(SupportSQLiteDatabase db) {
+ String identityHash = "";
+ if (hasRoomMasterTable(db)) {
+ Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ if (cursor.moveToFirst()) {
+ identityHash = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
+ throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ + " you've changed schema but forgot to update the version number. You can"
+ + " simply fix this by increasing the version number.");
+ }
+ }
+
+ private void updateIdentity(SupportSQLiteDatabase db) {
+ createMasterTableIfNotExists(db);
+ db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
+ }
+
+ private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
+ db.execSQL(RoomMasterTable.CREATE_QUERY);
+ }
+
+ private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
+ Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
+ + RoomMasterTable.TABLE_NAME + "'");
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ return cursor.moveToFirst() && cursor.getInt(0) != 0;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @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);
+
+ protected abstract void onOpen(SupportSQLiteDatabase database);
+
+ protected abstract void onCreate(SupportSQLiteDatabase database);
+
+ /**
+ * Called after a migration run to validate database integrity.
+ *
+ * @param db The SQLite database.
+ */
+ protected abstract void validateMigration(SupportSQLiteDatabase db);
+ }
+
+}
diff --git a/room/runtime/src/main/java/androidx/room/RoomSQLiteQuery.java b/room/runtime/src/main/java/androidx/room/RoomSQLiteQuery.java
new file mode 100644
index 0000000..08db4a4
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/RoomSQLiteQuery.java
@@ -0,0 +1,299 @@
+/*
+ * 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 androidx.room;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.sqlite.db.SupportSQLiteProgram;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * This class is used as an intermediate place to keep binding arguments so that we can run
+ * Cursor queries with correct types rather than passing everything as a string.
+ * <p>
+ * Because it is relatively a big object, they are pooled and must be released after each use.
+ *
+ * @hide
+ */
+@SuppressWarnings("unused")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ // Maximum number of queries we'll keep cached.
+ static final int POOL_LIMIT = 15;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
+ // clear the bigger queries (# of arguments).
+ static final int DESIRED_POOL_SIZE = 10;
+ private volatile String mQuery;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final long[] mLongBindings;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final double[] mDoubleBindings;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final String[] mStringBindings;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final byte[][] mBlobBindings;
+
+ @Binding
+ private final int[] mBindingTypes;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final int mCapacity;
+ // number of arguments in the query
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ int mArgCount;
+
+
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
+
+ /**
+ * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
+ *
+ * @param supportSQLiteQuery The query to copy from
+ * @return A new query copied from the provided one.
+ */
+ public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
+ final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
+ supportSQLiteQuery.getSql(),
+ supportSQLiteQuery.getArgCount());
+ supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
+ @Override
+ public void bindNull(int index) {
+ query.bindNull(index);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ query.bindLong(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ query.bindDouble(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ query.bindString(index, value);
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ query.bindBlob(index, value);
+ }
+
+ @Override
+ public void clearBindings() {
+ query.clearBindings();
+ }
+
+ @Override
+ public void close() {
+ // ignored.
+ }
+ });
+ return query;
+ }
+
+ /**
+ * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
+ * given query.
+ *
+ * @param query The query to prepare
+ * @param argumentCount The number of query arguments
+ * @return A RoomSQLiteQuery that holds the given query and has space for the given number of
+ * arguments.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static RoomSQLiteQuery acquire(String query, int argumentCount) {
+ synchronized (sQueryPool) {
+ final Map.Entry<Integer, RoomSQLiteQuery> entry =
+ sQueryPool.ceilingEntry(argumentCount);
+ if (entry != null) {
+ sQueryPool.remove(entry.getKey());
+ final RoomSQLiteQuery sqliteQuery = entry.getValue();
+ sqliteQuery.init(query, argumentCount);
+ return sqliteQuery;
+ }
+ }
+ RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
+ sqLiteQuery.init(query, argumentCount);
+ return sqLiteQuery;
+ }
+
+ private RoomSQLiteQuery(int capacity) {
+ mCapacity = capacity;
+ // because, 1 based indices... we don't want to offsets everything with 1 all the time.
+ int limit = capacity + 1;
+ //noinspection WrongConstant
+ mBindingTypes = new int[limit];
+ mLongBindings = new long[limit];
+ mDoubleBindings = new double[limit];
+ mStringBindings = new String[limit];
+ mBlobBindings = new byte[limit][];
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ void init(String query, int argCount) {
+ mQuery = query;
+ mArgCount = argCount;
+ }
+
+ /**
+ * Releases the query back to the pool.
+ * <p>
+ * After released, the statement might be returned when {@link #acquire(String, int)} is called
+ * so you should never re-use it after releasing.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void release() {
+ synchronized (sQueryPool) {
+ sQueryPool.put(mCapacity, this);
+ prunePoolLocked();
+ }
+ }
+
+ private static void prunePoolLocked() {
+ if (sQueryPool.size() > POOL_LIMIT) {
+ int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
+ final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
+ while (toBeRemoved-- > 0) {
+ iterator.next();
+ iterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public String getSql() {
+ return mQuery;
+ }
+
+ @Override
+ public int getArgCount() {
+ return mArgCount;
+ }
+
+ @Override
+ public void bindTo(SupportSQLiteProgram program) {
+ for (int index = 1; index <= mArgCount; index++) {
+ switch (mBindingTypes[index]) {
+ case NULL:
+ program.bindNull(index);
+ break;
+ case LONG:
+ program.bindLong(index, mLongBindings[index]);
+ break;
+ case DOUBLE:
+ program.bindDouble(index, mDoubleBindings[index]);
+ break;
+ case STRING:
+ program.bindString(index, mStringBindings[index]);
+ break;
+ case BLOB:
+ program.bindBlob(index, mBlobBindings[index]);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void bindNull(int index) {
+ mBindingTypes[index] = NULL;
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ mBindingTypes[index] = LONG;
+ mLongBindings[index] = value;
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ mBindingTypes[index] = DOUBLE;
+ mDoubleBindings[index] = value;
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ mBindingTypes[index] = STRING;
+ mStringBindings[index] = value;
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ mBindingTypes[index] = BLOB;
+ mBlobBindings[index] = value;
+ }
+
+ @Override
+ public void close() {
+ // no-op. not calling release because it is internal API.
+ }
+
+ /**
+ * Copies arguments from another RoomSQLiteQuery into this query.
+ *
+ * @param other The other query, which holds the arguments to be copied.
+ */
+ public void copyArgumentsFrom(RoomSQLiteQuery other) {
+ int argCount = other.getArgCount() + 1; // +1 for the binding offsets
+ System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
+ System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
+ System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
+ System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
+ System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
+ }
+
+ @Override
+ public void clearBindings() {
+ Arrays.fill(mBindingTypes, NULL);
+ Arrays.fill(mStringBindings, null);
+ Arrays.fill(mBlobBindings, null);
+ mQuery = null;
+ // no need to clear others
+ }
+
+ private static final int NULL = 1;
+ private static final int LONG = 2;
+ private static final int DOUBLE = 3;
+ private static final int STRING = 4;
+ private static final int BLOB = 5;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
+ @interface Binding {
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/SharedSQLiteStatement.java b/room/runtime/src/main/java/androidx/room/SharedSQLiteStatement.java
new file mode 100644
index 0000000..57d3340
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/SharedSQLiteStatement.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Represents a prepared SQLite state that can be re-used multiple times.
+ * <p>
+ * This class is used by generated code. After it is used, {@code release} must be called so that
+ * it can be used by other threads.
+ * <p>
+ * To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
+ * statement until it is released.
+ *
+ * @hide
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class SharedSQLiteStatement {
+ private final AtomicBoolean mLock = new AtomicBoolean(false);
+
+ private final RoomDatabase mDatabase;
+ private volatile SupportSQLiteStatement mStmt;
+
+ /**
+ * Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
+ * it automatically creates a new one.
+ *
+ * @param database The database to create the statement in.
+ */
+ public SharedSQLiteStatement(RoomDatabase database) {
+ mDatabase = database;
+ }
+
+ /**
+ * Create the query.
+ *
+ * @return The SQL query to prepare.
+ */
+ protected abstract String createQuery();
+
+ protected void assertNotMainThread() {
+ mDatabase.assertNotMainThread();
+ }
+
+ private SupportSQLiteStatement createNewStatement() {
+ String query = createQuery();
+ return mDatabase.compileStatement(query);
+ }
+
+ private SupportSQLiteStatement getStmt(boolean canUseCached) {
+ final SupportSQLiteStatement stmt;
+ if (canUseCached) {
+ if (mStmt == null) {
+ mStmt = createNewStatement();
+ }
+ stmt = mStmt;
+ } else {
+ // it is in use, create a one off statement
+ stmt = createNewStatement();
+ }
+ return stmt;
+ }
+
+ /**
+ * Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
+ */
+ public SupportSQLiteStatement acquire() {
+ assertNotMainThread();
+ return getStmt(mLock.compareAndSet(false, true));
+ }
+
+ /**
+ * Must call this when statement will not be used anymore.
+ *
+ * @param statement The statement that was returned from acquire.
+ */
+ public void release(SupportSQLiteStatement statement) {
+ if (statement == mStmt) {
+ mLock.set(false);
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/migration/Migration.java b/room/runtime/src/main/java/androidx/room/migration/Migration.java
new file mode 100644
index 0000000..4aa7a7e
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/migration/Migration.java
@@ -0,0 +1,63 @@
+/*
+ * 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 androidx.room.migration;
+
+import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+/**
+ * Base class for a database migration.
+ * <p>
+ * Each migration can move between 2 versions that are defined by {@link #startVersion} and
+ * {@link #endVersion}.
+ * <p>
+ * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+ * going version 3 to 5 without going to version 4). If Room opens a database at version
+ * 3 and latest version is >= 5, Room will use the migration object that can migrate from
+ * 3 to 5 instead of 3 to 4 and 4 to 5.
+ * <p>
+ * If there are not enough migrations provided to move from the current version to the latest
+ * version, Room will clear the database and recreate so even if you have no changes between 2
+ * versions, you should still provide a Migration object to the builder.
+ */
+public abstract class Migration {
+ public final int startVersion;
+ public final int endVersion;
+
+ /**
+ * Creates a new migration between {@code startVersion} and {@code endVersion}.
+ *
+ * @param startVersion The start version of the database.
+ * @param endVersion The end version of the database after this migration is applied.
+ */
+ public Migration(int startVersion, int endVersion) {
+ this.startVersion = startVersion;
+ this.endVersion = endVersion;
+ }
+
+ /**
+ * Should run the necessary migrations.
+ * <p>
+ * This class cannot access any generated Dao in this method.
+ * <p>
+ * This method is already called inside a transaction and that transaction might actually be a
+ * composite transaction of all necessary {@code Migration}s.
+ *
+ * @param database The database instance
+ */
+ public abstract void migrate(@NonNull SupportSQLiteDatabase database);
+}
diff --git a/room/runtime/src/main/java/androidx/room/package-info.java b/room/runtime/src/main/java/androidx/room/package-info.java
new file mode 100644
index 0000000..b691f0a
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/package-info.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+/**
+ * Room is a Database Object Mapping library that makes it easy to access database on Android
+ * applications.
+ * <p>
+ * Rather than hiding the detail of SQLite, Room tries to embrace them by providing convenient APIs
+ * to query the database and also verify such queries at compile time. This allows you to access
+ * the full power of SQLite while having the type safety provided by Java SQL query builders.
+ * <p>
+ * There are 3 major components in Room.
+ * <ul>
+ * <li>{@link androidx.room.Database Database}: This annotation marks a
+ * class as a database. It should be an abstract class that extends
+ * {@link androidx.room.RoomDatabase RoomDatabase}. At runtime, you can acquire
+ * an instance of it via {@link androidx.room.Room#databaseBuilder(
+ * android.content.Context,java.lang.Class, java.lang.String) Room.databaseBuilder} or
+ * {@link androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context,
+ * java.lang.Class) Room.inMemoryDatabaseBuilder}.
+ * <p>
+ * This class defines the list of entities and data access objects in the database. It is
+ * also the main access point for the underlying connection.
+ * </li>
+ * <li>{@link androidx.room.Entity Entity}: This annotation marks a class as a
+ * database row. For each {@link androidx.room.Entity Entity}, a database table
+ * is created to hold the items. The Entity class must be referenced in the
+ * {@link androidx.room.Database#entities() Database#entities} array. Each field
+ * of the Entity (and its super class) is persisted in the database unless it is denoted
+ * otherwise (see {@link androidx.room.Entity Entity} docs for details).
+ * </li>
+ * <li>{@link androidx.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
+ * responsible for defining the methods that access the database. The class that is annotated
+ * with {@link androidx.room.Database Database} must have an abstract method
+ * that has 0 arguments and returns the class that is annotated with Dao. While generating the
+ * code at compile time, Room will generate an implementation of this class.
+ * <pre>
+ * Using Dao classes for database access rather than query builders or direct queries allows you
+ * to keep a separation between different components and easily mock the database access while
+ * testing your application.
+ * </li>
+ * </ul>
+ * Below is a sample of a simple database.
+ * <pre>
+ * // File: User.java
+ * {@literal @}Entity
+ * public class User {
+ * {@literal @}PrimaryKey
+ * private int uid;
+ * private String name;
+ * {@literal @}ColumnInfo(name = "last_name")
+ * private String lastName;
+ * // getters and setters are ignored for brevity but they are required for Room to work.
+ * }
+ * // File: UserDao.java
+ * {@literal @}Dao
+ * public interface UserDao {
+ * {@literal @}Query("SELECT * FROM user")
+ * List<User> loadAll();
+ * {@literal @}Query("SELECT * FROM user WHERE uid IN (:userIds)")
+ * List<User> loadAllByUserId(int... userIds);
+ * {@literal @}Query("SELECT * FROM user where name LIKE :first AND last_name LIKE :last LIMIT 1")
+ * User loadOneByNameAndLastName(String first, String last);
+ * {@literal @}Insert
+ * void insertAll(User... users);
+ * {@literal @}Delete
+ * void delete(User user);
+ * }
+ * // File: AppDatabase.java
+ * {@literal @}Database(entities = {User.java})
+ * public abstract class AppDatabase extends RoomDatabase {
+ * public abstract UserDao userDao();
+ * }
+ * </pre>
+ * You can create an instance of {@code AppDatabase} as follows:
+ * <pre>
+ * AppDatabase db = Room
+ * .databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name")
+ * .build();
+ * </pre>
+ * Since Room verifies your queries at compile time, it also detects information about which tables
+ * are accessed by the query or what columns are present in the response.
+ * <p>
+ * You can observe a particular table for changes using the
+ * {@link androidx.room.InvalidationTracker InvalidationTracker} class which you can
+ * acquire via {@link androidx.room.RoomDatabase#getInvalidationTracker()
+ * RoomDatabase.getInvalidationTracker}.
+ * <p>
+ * For convenience, Room allows you to return {@link androidx.lifecycle.LiveData
+ * LiveData} from {@link androidx.room.Query Query} methods. It will automatically
+ * observe the related tables as long as the {@code LiveData} has active observers.
+ * <pre>
+ * // This live data will automatically dispatch changes as the database changes.
+ * {@literal @}Query("SELECT * FROM user ORDER BY name LIMIT 5")
+ * LiveData<User> loadFirstFiveUsers();
+ * </pre>
+ * <p>
+ * You can also return arbitrary Java objects from your query results as long as the fields in the
+ * object match the list of columns in the query response. This makes it very easy to write
+ * applications that drive the UI from persistent storage.
+ * <pre>
+ * class IdAndFullName {
+ * public int uid;
+ * {@literal @}ColumnInfo(name = "full_name")
+ * public String fullName;
+ * }
+ * // DAO
+ * {@literal @}Query("SELECT uid, name || lastName as full_name FROM user")
+ * public IdAndFullName[] loadFullNames();
+ * </pre>
+ * If there is a mismatch between the query result and the POJO, Room will print a warning during
+ * compilation.
+ * <p>
+ * Please see the documentation of individual classes for details.
+ */
+package androidx.room;
diff --git a/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
new file mode 100644
index 0000000..282389c
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
@@ -0,0 +1,176 @@
+/*
+ * 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 androidx.room.paging;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.paging.PositionalDataSource;
+import androidx.room.InvalidationTracker;
+import androidx.room.RoomDatabase;
+import androidx.room.RoomSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A simple data source implementation that uses Limit & Offset to page the query.
+ * <p>
+ * This is NOT the most efficient way to do paging on SQLite. It is
+ * <a href="http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor">recommended</a> to use an indexed
+ * ORDER BY statement but that requires a more complex API. This solution is technically equal to
+ * receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
+ * never returns inconsistent data if it is invalidated.
+ *
+ * @param <T> Data type returned by the data source.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> {
+ private final RoomSQLiteQuery mSourceQuery;
+ private final String mCountQuery;
+ private final String mLimitOffsetQuery;
+ private final RoomDatabase mDb;
+ @SuppressWarnings("FieldCanBeLocal")
+ private final InvalidationTracker.Observer mObserver;
+ private final boolean mInTransaction;
+
+ protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
+ boolean inTransaction, String... tables) {
+ this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, tables);
+ }
+
+ protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
+ boolean inTransaction, String... tables) {
+ mDb = db;
+ mSourceQuery = query;
+ mInTransaction = inTransaction;
+ mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
+ mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
+ mObserver = new InvalidationTracker.Observer(tables) {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ invalidate();
+ }
+ };
+ db.getInvalidationTracker().addWeakObserver(mObserver);
+ }
+
+ /**
+ * Count number of rows query can return
+ */
+ @SuppressWarnings("WeakerAccess")
+ public int countItems() {
+ final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
+ mSourceQuery.getArgCount());
+ sqLiteQuery.copyArgumentsFrom(mSourceQuery);
+ Cursor cursor = mDb.query(sqLiteQuery);
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ }
+ return 0;
+ } finally {
+ cursor.close();
+ sqLiteQuery.release();
+ }
+ }
+
+ @Override
+ public boolean isInvalid() {
+ mDb.getInvalidationTracker().refreshVersionsSync();
+ return super.isInvalid();
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ protected abstract List<T> convertRows(Cursor cursor);
+
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
+ int totalCount = countItems();
+ if (totalCount == 0) {
+ callback.onResult(Collections.<T>emptyList(), 0, 0);
+ return;
+ }
+
+ // bound the size requested, based on known count
+ final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
+ final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
+
+ List<T> list = loadRange(firstLoadPosition, firstLoadSize);
+ if (list != null && list.size() == firstLoadSize) {
+ callback.onResult(list, firstLoadPosition, totalCount);
+ } else {
+ // null list, or size doesn't match request - DB modified between count and load
+ invalidate();
+ }
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback) {
+ List<T> list = loadRange(params.startPosition, params.loadSize);
+ if (list != null) {
+ callback.onResult(list);
+ } else {
+ invalidate();
+ }
+ }
+
+ /**
+ * Return the rows from startPos to startPos + loadCount
+ */
+ @Nullable
+ public List<T> loadRange(int startPosition, int loadCount) {
+ final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
+ mSourceQuery.getArgCount() + 2);
+ sqLiteQuery.copyArgumentsFrom(mSourceQuery);
+ sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
+ sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
+ if (mInTransaction) {
+ mDb.beginTransaction();
+ Cursor cursor = null;
+ try {
+ cursor = mDb.query(sqLiteQuery);
+ List<T> rows = convertRows(cursor);
+ mDb.setTransactionSuccessful();
+ return rows;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ mDb.endTransaction();
+ sqLiteQuery.release();
+ }
+ } else {
+ Cursor cursor = mDb.query(sqLiteQuery);
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ return convertRows(cursor);
+ } finally {
+ cursor.close();
+ sqLiteQuery.release();
+ }
+ }
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/util/StringUtil.java b/room/runtime/src/main/java/androidx/room/util/StringUtil.java
new file mode 100644
index 0000000..8988490
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/util/StringUtil.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.util;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * @hide
+ *
+ * String utilities for Room
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class StringUtil {
+
+ @SuppressWarnings("unused")
+ public static final String[] EMPTY_STRING_ARRAY = new String[0];
+ /**
+ * Returns a new StringBuilder to be used while producing SQL queries.
+ *
+ * @return A new or recycled StringBuilder
+ */
+ public static StringBuilder newStringBuilder() {
+ // TODO pool:
+ return new StringBuilder();
+ }
+
+ /**
+ * Adds bind variable placeholders (?) to the given string. Each placeholder is separated
+ * by a comma.
+ *
+ * @param builder The StringBuilder for the query
+ * @param count Number of placeholders
+ */
+ public static void appendPlaceholders(StringBuilder builder, int count) {
+ for (int i = 0; i < count; i++) {
+ builder.append("?");
+ if (i < count - 1) {
+ builder.append(",");
+ }
+ }
+ }
+ /**
+ * Splits a comma separated list of integers to integer list.
+ * <p>
+ * If an input is malformed, it is omitted from the result.
+ *
+ * @param input Comma separated list of integers.
+ * @return A List containing the integers or null if the input is null.
+ */
+ @Nullable
+ public static List<Integer> splitToIntList(@Nullable String input) {
+ if (input == null) {
+ return null;
+ }
+ List<Integer> result = new ArrayList<>();
+ StringTokenizer tokenizer = new StringTokenizer(input, ",");
+ while (tokenizer.hasMoreElements()) {
+ final String item = tokenizer.nextToken();
+ try {
+ result.add(Integer.parseInt(item));
+ } catch (NumberFormatException ex) {
+ Log.e("ROOM", "Malformed integer list", ex);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Joins the given list of integers into a comma separated list.
+ *
+ * @param input The list of integers.
+ * @return Comma separated string composed of integers in the list. If the list is null, return
+ * value is null.
+ */
+ @Nullable
+ public static String joinIntoString(@Nullable List<Integer> input) {
+ if (input == null) {
+ return null;
+ }
+
+ final int size = input.size();
+ if (size == 0) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < size; i++) {
+ sb.append(Integer.toString(input.get(i)));
+ if (i < size - 1) {
+ sb.append(",");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/util/TableInfo.java b/room/runtime/src/main/java/androidx/room/util/TableInfo.java
new file mode 100644
index 0000000..819c726
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/util/TableInfo.java
@@ -0,0 +1,592 @@
+/*
+ * 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 androidx.room.util;
+
+import android.database.Cursor;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.room.ColumnInfo;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * A data class that holds the information about a table.
+ * <p>
+ * It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
+ * <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
+ * documentation for more details.
+ * <p>
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
+ "SimplifiableIfStatement"})
+// if you change this class, you must change TableInfoWriter.kt
+public class TableInfo {
+ /**
+ * The table name.
+ */
+ public final String name;
+ /**
+ * Unmodifiable map of columns keyed by column name.
+ */
+ public final Map<String, Column> columns;
+
+ 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,
+ 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
+ + '}';
+ }
+
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param database The database to read the information from.
+ * @param tableName The table name.
+ * @return A TableInfo containing the schema information for the provided table name.
+ */
+ @SuppressWarnings("SameParameterValue")
+ public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
+ Map<String, Column> columns = readColumns(database, tableName);
+ Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
+ Set<Index> indices = readIndices(database, tableName);
+ return new TableInfo(tableName, columns, foreignKeys, indices);
+ }
+
+ private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
+ String tableName) {
+ Set<ForeignKey> foreignKeys = new HashSet<>();
+ // this seems to return everything in order but it is not documented so better be safe
+ Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
+ try {
+ final int idColumnIndex = cursor.getColumnIndex("id");
+ final int seqColumnIndex = cursor.getColumnIndex("seq");
+ final int tableColumnIndex = cursor.getColumnIndex("table");
+ final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
+ final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
+
+ final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor);
+ final int count = cursor.getCount();
+ for (int position = 0; position < count; position++) {
+ cursor.moveToPosition(position);
+ final int seq = cursor.getInt(seqColumnIndex);
+ if (seq != 0) {
+ continue;
+ }
+ final int id = cursor.getInt(idColumnIndex);
+ List<String> myColumns = new ArrayList<>();
+ List<String> refColumns = new ArrayList<>();
+ for (ForeignKeyWithSequence key : ordered) {
+ if (key.mId == id) {
+ myColumns.add(key.mFrom);
+ refColumns.add(key.mTo);
+ }
+ }
+ foreignKeys.add(new ForeignKey(
+ cursor.getString(tableColumnIndex),
+ cursor.getString(onDeleteColumnIndex),
+ cursor.getString(onUpdateColumnIndex),
+ myColumns,
+ refColumns
+ ));
+ }
+ } finally {
+ cursor.close();
+ }
+ return foreignKeys;
+ }
+
+ private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) {
+ final int idColumnIndex = cursor.getColumnIndex("id");
+ final int seqColumnIndex = cursor.getColumnIndex("seq");
+ final int fromColumnIndex = cursor.getColumnIndex("from");
+ final int toColumnIndex = cursor.getColumnIndex("to");
+ final int count = cursor.getCount();
+ List<ForeignKeyWithSequence> result = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ cursor.moveToPosition(i);
+ result.add(new ForeignKeyWithSequence(
+ cursor.getInt(idColumnIndex),
+ cursor.getInt(seqColumnIndex),
+ cursor.getString(fromColumnIndex),
+ cursor.getString(toColumnIndex)
+ ));
+ }
+ Collections.sort(result);
+ return result;
+ }
+
+ private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
+ String tableName) {
+ Cursor cursor = database
+ .query("PRAGMA table_info(`" + tableName + "`)");
+ //noinspection TryFinallyCanBeTryWithResources
+ Map<String, Column> columns = new HashMap<>();
+ try {
+ if (cursor.getColumnCount() > 0) {
+ int nameIndex = cursor.getColumnIndex("name");
+ int typeIndex = cursor.getColumnIndex("type");
+ int notNullIndex = cursor.getColumnIndex("notnull");
+ int pkIndex = cursor.getColumnIndex("pk");
+
+ while (cursor.moveToNext()) {
+ final String name = cursor.getString(nameIndex);
+ final String type = cursor.getString(typeIndex);
+ final boolean notNull = 0 != cursor.getInt(notNullIndex);
+ final int primaryKeyPosition = cursor.getInt(pkIndex);
+ columns.put(name, new Column(name, type, notNull, primaryKeyPosition));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return columns;
+ }
+
+ /**
+ * @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();
+ }
+ }
+
+ /**
+ * @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<>();
+
+ 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();
+ }
+ }
+
+ /**
+ * Holds the information about a database column.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class Column {
+ /**
+ * The column name.
+ */
+ public final String name;
+ /**
+ * The column type affinity.
+ */
+ public final String type;
+ /**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ * <p>
+ * This is the value Room uses for equality check.
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ public final int affinity;
+ /**
+ * Whether or not the column can be NULL.
+ */
+ public final boolean notNull;
+ /**
+ * The position of the column in the list of primary keys, 0 if the column is not part
+ * of the primary key.
+ * <p>
+ * This information is only available in API 20+.
+ * <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
+ * On older platforms, it will be 1 if the column is part of the primary key and 0
+ * otherwise.
+ * <p>
+ * The {@link #equals(Object)} implementation handles this inconsistency based on
+ * API levels os if you are using a custom SQLite deployment, it may return false
+ * positives.
+ */
+ public final int primaryKeyPosition;
+
+ // if you change this constructor, you must change TableInfoWriter.kt
+ public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
+ this.name = name;
+ this.type = type;
+ this.notNull = notNull;
+ this.primaryKeyPosition = primaryKeyPosition;
+ this.affinity = findAffinity(type);
+ }
+
+ /**
+ * Implements https://www.sqlite.org/datatype3.html section 3.1
+ *
+ * @param type The type that was given to the sqlite
+ * @return The normalized type which is one of the 5 known affinities
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ private static int findAffinity(@Nullable String type) {
+ if (type == null) {
+ return ColumnInfo.BLOB;
+ }
+ String uppercaseType = type.toUpperCase(Locale.US);
+ if (uppercaseType.contains("INT")) {
+ return ColumnInfo.INTEGER;
+ }
+ if (uppercaseType.contains("CHAR")
+ || uppercaseType.contains("CLOB")
+ || uppercaseType.contains("TEXT")) {
+ return ColumnInfo.TEXT;
+ }
+ if (uppercaseType.contains("BLOB")) {
+ return ColumnInfo.BLOB;
+ }
+ if (uppercaseType.contains("REAL")
+ || uppercaseType.contains("FLOA")
+ || uppercaseType.contains("DOUB")) {
+ return ColumnInfo.REAL;
+ }
+ // sqlite returns NUMERIC here but it is like a catch all. We already
+ // have UNDEFINED so it is better to use UNDEFINED for consistency.
+ return ColumnInfo.UNDEFINED;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Column column = (Column) o;
+ if (Build.VERSION.SDK_INT >= 20) {
+ if (primaryKeyPosition != column.primaryKeyPosition) return false;
+ } else {
+ if (isPrimaryKey() != column.isPrimaryKey()) return false;
+ }
+
+ if (!name.equals(column.name)) return false;
+ //noinspection SimplifiableIfStatement
+ if (notNull != column.notNull) return false;
+ return affinity == column.affinity;
+ }
+
+ /**
+ * Returns whether this column is part of the primary key or not.
+ *
+ * @return True if this column is part of the primary key, false otherwise.
+ */
+ public boolean isPrimaryKey() {
+ return primaryKeyPosition > 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name.hashCode();
+ result = 31 * result + affinity;
+ result = 31 * result + (notNull ? 1231 : 1237);
+ result = 31 * result + primaryKeyPosition;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Column{"
+ + "name='" + name + '\''
+ + ", type='" + type + '\''
+ + ", affinity='" + affinity + '\''
+ + ", notNull=" + notNull
+ + ", primaryKeyPosition=" + primaryKeyPosition
+ + '}';
+ }
+ }
+
+ /**
+ * Holds the information about an SQLite foreign key
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static class ForeignKey {
+ @NonNull
+ public final String referenceTable;
+ @NonNull
+ public final String onDelete;
+ @NonNull
+ public final String onUpdate;
+ @NonNull
+ public final List<String> columnNames;
+ @NonNull
+ public final List<String> referenceColumnNames;
+
+ public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
+ @NonNull String onUpdate,
+ @NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) {
+ this.referenceTable = referenceTable;
+ this.onDelete = onDelete;
+ this.onUpdate = onUpdate;
+ this.columnNames = Collections.unmodifiableList(columnNames);
+ this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ForeignKey that = (ForeignKey) o;
+
+ if (!referenceTable.equals(that.referenceTable)) return false;
+ if (!onDelete.equals(that.onDelete)) return false;
+ if (!onUpdate.equals(that.onUpdate)) return false;
+ //noinspection SimplifiableIfStatement
+ if (!columnNames.equals(that.columnNames)) return false;
+ return referenceColumnNames.equals(that.referenceColumnNames);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = referenceTable.hashCode();
+ result = 31 * result + onDelete.hashCode();
+ result = 31 * result + onUpdate.hashCode();
+ result = 31 * result + columnNames.hashCode();
+ result = 31 * result + referenceColumnNames.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ForeignKey{"
+ + "referenceTable='" + referenceTable + '\''
+ + ", onDelete='" + onDelete + '\''
+ + ", onUpdate='" + onUpdate + '\''
+ + ", columnNames=" + columnNames
+ + ", referenceColumnNames=" + referenceColumnNames
+ + '}';
+ }
+ }
+
+ /**
+ * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
+ * sorting in the generated foreign key object.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> {
+ final int mId;
+ final int mSequence;
+ final String mFrom;
+ final String mTo;
+
+ ForeignKeyWithSequence(int id, int sequence, String from, String to) {
+ mId = id;
+ mSequence = sequence;
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ public int compareTo(@NonNull ForeignKeyWithSequence o) {
+ final int idCmp = mId - o.mId;
+ if (idCmp == 0) {
+ return mSequence - o.mSequence;
+ } else {
+ return idCmp;
+ }
+ }
+ }
+
+ /**
+ * 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
+ + '}';
+ }
+ }
+}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java b/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
deleted file mode 100644
index 215bc30..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * Copyright (C) 2016 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 static org.hamcrest.CoreMatchers.instanceOf;
-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 static org.mockito.Mockito.mock;
-
-import static java.util.Arrays.asList;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
-import android.arch.persistence.room.migration.Migration;
-import android.content.Context;
-import android.support.annotation.NonNull;
-
-import org.hamcrest.CoreMatchers;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.List;
-
-@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
-@RunWith(JUnit4.class)
-public class BuilderTest {
- @Test(expected = IllegalArgumentException.class)
- public void nullContext() {
- //noinspection ConstantConditions
- Room.databaseBuilder(null, RoomDatabase.class, "bla").build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void nullContext2() {
- //noinspection ConstantConditions
- Room.inMemoryDatabaseBuilder(null, RoomDatabase.class).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void nullName() {
- //noinspection ConstantConditions
- Room.databaseBuilder(mock(Context.class), RoomDatabase.class, null).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void emptyName() {
- Room.databaseBuilder(mock(Context.class), RoomDatabase.class, " ").build();
- }
-
- @Test
- public void migration() {
- Migration m1 = new EmptyMigration(0, 1);
- Migration m2 = new EmptyMigration(1, 2);
- TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
- .addMigrations(m1, m2).build();
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- RoomDatabase.MigrationContainer migrations = config.migrationContainer;
- assertThat(migrations.findMigrationPath(0, 1), is(asList(m1)));
- assertThat(migrations.findMigrationPath(1, 2), is(asList(m2)));
- assertThat(migrations.findMigrationPath(0, 2), is(asList(m1, m2)));
- assertThat(migrations.findMigrationPath(2, 0), CoreMatchers.<List<Migration>>nullValue());
- assertThat(migrations.findMigrationPath(0, 3), CoreMatchers.<List<Migration>>nullValue());
- }
-
- @Test
- public void migrationOverride() {
- Migration m1 = new EmptyMigration(0, 1);
- Migration m2 = new EmptyMigration(1, 2);
- Migration m3 = new EmptyMigration(0, 1);
- TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
- .addMigrations(m1, m2, m3).build();
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- RoomDatabase.MigrationContainer migrations = config.migrationContainer;
- assertThat(migrations.findMigrationPath(0, 1), is(asList(m3)));
- assertThat(migrations.findMigrationPath(1, 2), is(asList(m2)));
- assertThat(migrations.findMigrationPath(0, 3), CoreMatchers.<List<Migration>>nullValue());
- }
-
- @Test
- public void migrationJump() {
- Migration m1 = new EmptyMigration(0, 1);
- Migration m2 = new EmptyMigration(1, 2);
- Migration m3 = new EmptyMigration(2, 3);
- Migration m4 = new EmptyMigration(0, 3);
- TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
- .addMigrations(m1, m2, m3, m4).build();
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- RoomDatabase.MigrationContainer migrations = config.migrationContainer;
- assertThat(migrations.findMigrationPath(0, 3), is(asList(m4)));
- assertThat(migrations.findMigrationPath(1, 3), is(asList(m2, m3)));
- }
-
- @Test
- public void migrationDowngrade() {
- Migration m1_2 = new EmptyMigration(1, 2);
- Migration m2_3 = new EmptyMigration(2, 3);
- Migration m3_4 = new EmptyMigration(3, 4);
- Migration m3_2 = new EmptyMigration(3, 2);
- Migration m2_1 = new EmptyMigration(2, 1);
- TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
- .addMigrations(m1_2, m2_3, m3_4, m3_2, m2_1).build();
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- RoomDatabase.MigrationContainer migrations = config.migrationContainer;
- assertThat(migrations.findMigrationPath(3, 2), is(asList(m3_2)));
- assertThat(migrations.findMigrationPath(3, 1), is(asList(m3_2, m2_1)));
- }
-
- @Test
- public void skipMigration() {
- Context context = mock(Context.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigration()
- .build();
-
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.requireMigration, is(false));
- }
-
- @Test
- public void fallbackToDestructiveMigrationFrom_calledOnce_migrationsNotRequiredForValues() {
- Context context = mock(Context.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigrationFrom(1, 2).build();
-
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.isMigrationRequiredFrom(1), is(false));
- assertThat(config.isMigrationRequiredFrom(2), is(false));
- }
-
- @Test
- public void fallbackToDestructiveMigrationFrom_calledTwice_migrationsNotRequiredForValues() {
- Context context = mock(Context.class);
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigrationFrom(1, 2)
- .fallbackToDestructiveMigrationFrom(3, 4)
- .build();
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
-
- assertThat(config.isMigrationRequiredFrom(1), is(false));
- assertThat(config.isMigrationRequiredFrom(2), is(false));
- assertThat(config.isMigrationRequiredFrom(3), is(false));
- assertThat(config.isMigrationRequiredFrom(4), is(false));
- }
-
- @Test
- public void isMigrationRequiredFrom_fallBackToDestructiveCalled_alwaysReturnsFalse() {
- Context context = mock(Context.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigration()
- .build();
-
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.isMigrationRequiredFrom(0), is(false));
- assertThat(config.isMigrationRequiredFrom(1), is(false));
- assertThat(config.isMigrationRequiredFrom(5), is(false));
- assertThat(config.isMigrationRequiredFrom(12), is(false));
- assertThat(config.isMigrationRequiredFrom(132), is(false));
- }
-
- @Test
- public void isMigrationRequiredFrom_byDefault_alwaysReturnsTrue() {
- Context context = mock(Context.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .build();
-
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.isMigrationRequiredFrom(0), is(true));
- assertThat(config.isMigrationRequiredFrom(1), is(true));
- assertThat(config.isMigrationRequiredFrom(5), is(true));
- assertThat(config.isMigrationRequiredFrom(12), is(true));
- assertThat(config.isMigrationRequiredFrom(132), is(true));
- }
-
- @Test
- public void isMigrationRequiredFrom_fallBackToDestFromCalled_falseForProvidedValues() {
- Context context = mock(Context.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigrationFrom(1, 4, 81)
- .build();
-
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.isMigrationRequiredFrom(1), is(false));
- assertThat(config.isMigrationRequiredFrom(4), is(false));
- assertThat(config.isMigrationRequiredFrom(81), is(false));
- }
-
- @Test
- public void isMigrationRequiredFrom_fallBackToDestFromCalled_trueForNonProvidedValues() {
- Context context = mock(Context.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigrationFrom(1, 4, 81)
- .build();
-
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.isMigrationRequiredFrom(2), is(true));
- assertThat(config.isMigrationRequiredFrom(3), is(true));
- assertThat(config.isMigrationRequiredFrom(73), is(true));
- }
-
- @Test
- public void createBasic() {
- Context context = mock(Context.class);
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
- assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config, notNullValue());
- assertThat(config.context, is(context));
- assertThat(config.name, is(nullValue()));
- assertThat(config.allowMainThreadQueries, is(false));
- assertThat(config.journalMode, is(RoomDatabase.JournalMode.TRUNCATE));
- assertThat(config.sqliteOpenHelperFactory,
- instanceOf(FrameworkSQLiteOpenHelperFactory.class));
- }
-
- @Test
- public void createAllowMainThread() {
- Context context = mock(Context.class);
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .allowMainThreadQueries()
- .build();
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.allowMainThreadQueries, is(true));
- }
-
- @Test
- public void createWriteAheadLogging() {
- Context context = mock(Context.class);
- TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "foo")
- .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING).build();
- assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config.journalMode, is(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING));
- }
-
- @Test
- public void createWithFactoryAndVersion() {
- Context context = mock(Context.class);
- SupportSQLiteOpenHelper.Factory factory = mock(SupportSQLiteOpenHelper.Factory.class);
-
- TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .openHelperFactory(factory)
- .build();
- assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
- DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
- assertThat(config, notNullValue());
- assertThat(config.sqliteOpenHelperFactory, is(factory));
- }
-
- abstract static class TestDatabase extends RoomDatabase {
- }
-
- static class EmptyMigration extends Migration {
- EmptyMigration(int start, int end) {
- super(start, end);
- }
-
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- }
- }
-
-}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest_TestDatabase_Impl.java b/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest_TestDatabase_Impl.java
deleted file mode 100644
index 8e310e2..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest_TestDatabase_Impl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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 android.arch.persistence.db.SupportSQLiteOpenHelper;
-
-public class BuilderTest_TestDatabase_Impl extends BuilderTest.TestDatabase {
- DatabaseConfiguration mConfig;
- @Override
- public void init(DatabaseConfiguration configuration) {
- super.init(configuration);
- mConfig = configuration;
- }
-
- @Override
- protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
- return null;
- }
-
- @Override
- protected InvalidationTracker createInvalidationTracker() {
- return null;
- }
-
- @Override
- public void clearAllTables() {
- }
-}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java b/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
deleted file mode 100644
index ca091d5..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
+++ /dev/null
@@ -1,339 +0,0 @@
-/*
- * 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 static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsCollectionContaining.hasItem;
-import static org.hamcrest.core.IsCollectionContaining.hasItems;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.arch.core.executor.JunitTaskExecutorRule;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.SupportSQLiteStatement;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-import android.support.annotation.NonNull;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
-
-@RunWith(JUnit4.class)
-public class InvalidationTrackerTest {
- private InvalidationTracker mTracker;
- private RoomDatabase mRoomDatabase;
- private SupportSQLiteOpenHelper mOpenHelper;
- @Rule
- public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1, true);
-
- @Before
- public void setup() {
- mRoomDatabase = mock(RoomDatabase.class);
- SupportSQLiteDatabase sqliteDb = mock(SupportSQLiteDatabase.class);
- final SupportSQLiteStatement statement = mock(SupportSQLiteStatement.class);
- mOpenHelper = mock(SupportSQLiteOpenHelper.class);
-
- doReturn(statement).when(sqliteDb).compileStatement(eq(InvalidationTracker.CLEANUP_SQL));
- doReturn(sqliteDb).when(mOpenHelper).getWritableDatabase();
- doReturn(true).when(mRoomDatabase).isOpen();
- ReentrantLock closeLock = new ReentrantLock();
- doReturn(closeLock).when(mRoomDatabase).getCloseLock();
- //noinspection ResultOfMethodCallIgnored
- doReturn(mOpenHelper).when(mRoomDatabase).getOpenHelper();
-
- mTracker = new InvalidationTracker(mRoomDatabase, "a", "B", "i");
- mTracker.internalInit(sqliteDb);
- }
-
- @Before
- public void setLocale() {
- Locale.setDefault(Locale.forLanguageTag("tr-TR"));
- }
-
- @After
- public void unsetLocale() {
- Locale.setDefault(Locale.US);
- }
-
- @Test
- public void tableIds() {
- assertThat(mTracker.mTableIdLookup.get("a"), is(0));
- assertThat(mTracker.mTableIdLookup.get("b"), is(1));
- }
-
- @Test
- public void testWeak() throws InterruptedException {
- final AtomicInteger data = new AtomicInteger(0);
- InvalidationTracker.Observer observer = new InvalidationTracker.Observer("a") {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- data.incrementAndGet();
- }
- };
- mTracker.addWeakObserver(observer);
- setVersions(1, 0);
- refreshSync();
- assertThat(data.get(), is(1));
- observer = null;
- forceGc();
- setVersions(2, 0);
- refreshSync();
- assertThat(data.get(), is(1));
- }
-
- @Test
- public void addRemoveObserver() throws Exception {
- InvalidationTracker.Observer observer = new LatchObserver(1, "a");
- mTracker.addObserver(observer);
- assertThat(mTracker.mObserverMap.size(), is(1));
- mTracker.removeObserver(new LatchObserver(1, "a"));
- assertThat(mTracker.mObserverMap.size(), is(1));
- mTracker.removeObserver(observer);
- assertThat(mTracker.mObserverMap.size(), is(0));
- }
-
- private void drainTasks() throws InterruptedException {
- mTaskExecutorRule.drainTasks(200);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void badObserver() {
- InvalidationTracker.Observer observer = new LatchObserver(1, "x");
- mTracker.addObserver(observer);
- }
-
- @Test
- public void refreshReadValues() throws Exception {
- setVersions(1, 0, 2, 1);
- refreshSync();
- assertThat(mTracker.mTableVersions, is(new long[]{1, 2, 0}));
-
- setVersions(3, 1);
- refreshSync();
- assertThat(mTracker.mTableVersions, is(new long[]{1, 3, 0}));
-
- setVersions(7, 0);
- refreshSync();
- assertThat(mTracker.mTableVersions, is(new long[]{7, 3, 0}));
-
- refreshSync();
- assertThat(mTracker.mTableVersions, is(new long[]{7, 3, 0}));
- }
-
- private void refreshSync() throws InterruptedException {
- mTracker.refreshVersionsAsync();
- drainTasks();
- }
-
- @Test
- public void refreshCheckTasks() throws Exception {
- when(mRoomDatabase.query(anyString(), any(Object[].class)))
- .thenReturn(mock(Cursor.class));
- mTracker.refreshVersionsAsync();
- mTracker.refreshVersionsAsync();
- verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
- drainTasks();
-
- reset(mTaskExecutorRule.getTaskExecutor());
- mTracker.refreshVersionsAsync();
- verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
- }
-
- @Test
- public void observe1Table() throws Exception {
- LatchObserver observer = new LatchObserver(1, "a");
- mTracker.addObserver(observer);
- setVersions(1, 0, 2, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("a"));
-
- setVersions(3, 1);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
-
- setVersions(4, 0);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("a"));
- }
-
- @Test
- public void observe2Tables() throws Exception {
- LatchObserver observer = new LatchObserver(1, "A", "B");
- mTracker.addObserver(observer);
- setVersions(1, 0, 2, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(2));
- assertThat(observer.getInvalidatedTables(), hasItems("A", "B"));
-
- setVersions(3, 1);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("B"));
-
- setVersions(4, 0);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("A"));
-
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
- }
-
- @Test
- public void locale() {
- LatchObserver observer = new LatchObserver(1, "I");
- mTracker.addObserver(observer);
- }
-
- @Test
- public void closedDb() {
- doReturn(false).when(mRoomDatabase).isOpen();
- doThrow(new IllegalStateException("foo")).when(mOpenHelper).getWritableDatabase();
- mTracker.addObserver(new LatchObserver(1, "a", "b"));
- mTracker.mRefreshRunnable.run();
- }
-
- // @Test - disabled due to flakiness b/65257997
- public void closedDbAfterOpen() throws InterruptedException {
- setVersions(3, 1);
- mTracker.addObserver(new LatchObserver(1, "a", "b"));
- mTracker.syncTriggers();
- mTracker.mRefreshRunnable.run();
- doThrow(new SQLiteException("foo")).when(mRoomDatabase).query(
- Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
- any(Object[].class));
- mTracker.mPendingRefresh.set(true);
- mTracker.mRefreshRunnable.run();
- }
-
- /**
- * Key value pairs of VERSION, TABLE_ID
- */
- private void setVersions(int... keyValuePairs) throws InterruptedException {
- // mockito does not like multi-threaded access so before setting versions, make sure we
- // sync background tasks.
- drainTasks();
- Cursor cursor = createCursorWithValues(keyValuePairs);
- doReturn(cursor).when(mRoomDatabase).query(
- Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
- any(Object[].class)
- );
- }
-
- private Cursor createCursorWithValues(final int... keyValuePairs) {
- Cursor cursor = mock(Cursor.class);
- final AtomicInteger index = new AtomicInteger(-2);
- when(cursor.moveToNext()).thenAnswer(new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
- return index.addAndGet(2) < keyValuePairs.length;
- }
- });
- Answer<Integer> intAnswer = new Answer<Integer>() {
- @Override
- public Integer answer(InvocationOnMock invocation) throws Throwable {
- return keyValuePairs[index.intValue() + (Integer) invocation.getArguments()[0]];
- }
- };
- Answer<Long> longAnswer = new Answer<Long>() {
- @Override
- public Long answer(InvocationOnMock invocation) throws Throwable {
- return (long) keyValuePairs[index.intValue()
- + (Integer) invocation.getArguments()[0]];
- }
- };
- when(cursor.getInt(anyInt())).thenAnswer(intAnswer);
- when(cursor.getLong(anyInt())).thenAnswer(longAnswer);
- return cursor;
- }
-
- static class LatchObserver extends InvalidationTracker.Observer {
- private CountDownLatch mLatch;
- private Set<String> mInvalidatedTables;
-
- LatchObserver(int count, String... tableNames) {
- super(tableNames);
- mLatch = new CountDownLatch(count);
- }
-
- boolean await() throws InterruptedException {
- return mLatch.await(3, TimeUnit.SECONDS);
- }
-
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- mInvalidatedTables = tables;
- mLatch.countDown();
- }
-
- void reset(@SuppressWarnings("SameParameterValue") int count) {
- mInvalidatedTables = null;
- mLatch = new CountDownLatch(count);
- }
-
- Set<String> getInvalidatedTables() {
- return mInvalidatedTables;
- }
- }
-
- private static void forceGc() {
- // Use a random index in the list to detect the garbage collection each time because
- // .get() may accidentally trigger a strong reference during collection.
- ArrayList<WeakReference<byte[]>> leak = new ArrayList<>();
- do {
- WeakReference<byte[]> arr = new WeakReference<>(new byte[100]);
- leak.add(arr);
- } while (leak.get((int) (Math.random() * leak.size())).get() != null);
- }
-}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/ObservedTableTrackerTest.java b/room/runtime/src/test/java/android/arch/persistence/room/ObservedTableTrackerTest.java
deleted file mode 100644
index ffddee9..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/ObservedTableTrackerTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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 static android.arch.persistence.room.InvalidationTracker.ObservedTableTracker.ADD;
-import static android.arch.persistence.room.InvalidationTracker.ObservedTableTracker.NO_OP;
-import static android.arch.persistence.room.InvalidationTracker.ObservedTableTracker.REMOVE;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-
-@RunWith(JUnit4.class)
-public class ObservedTableTrackerTest {
- private static final int TABLE_COUNT = 5;
- private InvalidationTracker.ObservedTableTracker mTracker;
-
- @Before
- public void setup() {
- mTracker = new InvalidationTracker.ObservedTableTracker(TABLE_COUNT);
- }
-
- @Test
- public void basicAdd() {
- mTracker.onAdded(2, 3);
- assertThat(mTracker.getTablesToSync(), is(createResponse(2, ADD, 3, ADD)));
- }
-
- @Test
- public void basicRemove() {
- initState(2, 3);
- mTracker.onRemoved(3);
- assertThat(mTracker.getTablesToSync(), is(createResponse(3, REMOVE)));
- }
-
- @Test
- public void noChange() {
- initState(1, 3);
- mTracker.onAdded(3);
- assertThat(mTracker.getTablesToSync(), is(nullValue()));
- }
-
- @Test
- public void returnNullUntilSync() {
- initState(1, 3);
- mTracker.onAdded(4);
- assertThat(mTracker.getTablesToSync(), is(createResponse(4, ADD)));
- mTracker.onAdded(0);
- assertThat(mTracker.getTablesToSync(), is(nullValue()));
- mTracker.onSyncCompleted();
- assertThat(mTracker.getTablesToSync(), is(createResponse(0, ADD)));
- }
-
- @Test
- public void multipleAdditionsDeletions() {
- initState(2, 4);
- mTracker.onAdded(2);
- assertThat(mTracker.getTablesToSync(), is(nullValue()));
- mTracker.onAdded(2, 4);
- assertThat(mTracker.getTablesToSync(), is(nullValue()));
- mTracker.onRemoved(2);
- assertThat(mTracker.getTablesToSync(), is(nullValue()));
- mTracker.onRemoved(2, 4);
- assertThat(mTracker.getTablesToSync(), is(nullValue()));
- mTracker.onAdded(1, 3);
- mTracker.onRemoved(2, 4);
- assertThat(mTracker.getTablesToSync(), is(
- createResponse(1, ADD, 2, REMOVE, 3, ADD, 4, REMOVE)));
- }
-
- private void initState(int... tableIds) {
- mTracker.onAdded(tableIds);
- mTracker.getTablesToSync();
- mTracker.onSyncCompleted();
- }
-
- private static int[] createResponse(int... tuples) {
- int[] result = new int[TABLE_COUNT];
- Arrays.fill(result, NO_OP);
- for (int i = 0; i < tuples.length; i += 2) {
- result[tuples[i]] = tuples[i + 1];
- }
- return result;
- }
-}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/RoomSQLiteQueryTest.java b/room/runtime/src/test/java/android/arch/persistence/room/RoomSQLiteQueryTest.java
deleted file mode 100644
index e7a8644..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/RoomSQLiteQueryTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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 static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.sameInstance;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.arch.persistence.db.SupportSQLiteProgram;
-
-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.Iterator;
-import java.util.List;
-
-@RunWith(JUnit4.class)
-public class RoomSQLiteQueryTest {
- @Before
- public void clear() {
- RoomSQLiteQuery.sQueryPool.clear();
- }
-
- @Test
- public void acquireBasic() {
- RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
- assertThat(query.getSql(), is("abc"));
- assertThat(query.mArgCount, is(3));
- assertThat(query.mBlobBindings.length, is(4));
- assertThat(query.mLongBindings.length, is(4));
- assertThat(query.mStringBindings.length, is(4));
- assertThat(query.mDoubleBindings.length, is(4));
- }
-
- @Test
- public void acquireSameSizeAgain() {
- RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
- query.release();
- assertThat(RoomSQLiteQuery.acquire("blah", 3), sameInstance(query));
- }
-
- @Test
- public void acquireSameSizeWithoutRelease() {
- RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
- assertThat(RoomSQLiteQuery.acquire("fda", 3), not(sameInstance(query)));
- }
-
- @Test
- public void bindings() {
- RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 6);
- byte[] myBlob = new byte[3];
- long myLong = 3L;
- double myDouble = 7.0;
- String myString = "ss";
- query.bindBlob(1, myBlob);
- query.bindLong(2, myLong);
- query.bindNull(3);
- query.bindDouble(4, myDouble);
- query.bindString(5, myString);
- query.bindNull(6);
- SupportSQLiteProgram program = mock(SupportSQLiteProgram.class);
- query.bindTo(program);
-
- verify(program).bindBlob(1, myBlob);
- verify(program).bindLong(2, myLong);
- verify(program).bindNull(3);
- verify(program).bindDouble(4, myDouble);
- verify(program).bindString(5, myString);
- verify(program).bindNull(6);
- }
-
- @Test
- public void dontKeepSameSizeTwice() {
- RoomSQLiteQuery query1 = RoomSQLiteQuery.acquire("abc", 3);
- RoomSQLiteQuery query2 = RoomSQLiteQuery.acquire("zx", 3);
- RoomSQLiteQuery query3 = RoomSQLiteQuery.acquire("qw", 0);
-
- query1.release();
- query2.release();
- assertThat(RoomSQLiteQuery.sQueryPool.size(), is(1));
-
- query3.release();
- assertThat(RoomSQLiteQuery.sQueryPool.size(), is(2));
- }
-
- @Test
- public void returnExistingForSmallerSize() {
- RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
- query.release();
- assertThat(RoomSQLiteQuery.acquire("dsa", 2), sameInstance(query));
- }
-
- @Test
- public void returnNewForBigger() {
- RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
- query.release();
- assertThat(RoomSQLiteQuery.acquire("dsa", 4), not(sameInstance(query)));
- }
-
- @Test
- public void pruneCache() {
- for (int i = 0; i < RoomSQLiteQuery.POOL_LIMIT; i++) {
- RoomSQLiteQuery.acquire("dsdsa", i).release();
- }
- pruneCacheTest();
- }
-
- @Test
- public void pruneCacheReverseInsertion() {
- List<RoomSQLiteQuery> queries = new ArrayList<>();
- for (int i = RoomSQLiteQuery.POOL_LIMIT - 1; i >= 0; i--) {
- queries.add(RoomSQLiteQuery.acquire("dsdsa", i));
- }
- for (RoomSQLiteQuery query : queries) {
- query.release();
- }
- pruneCacheTest();
- }
-
- private void pruneCacheTest() {
- assertThat(RoomSQLiteQuery.sQueryPool.size(), is(RoomSQLiteQuery.POOL_LIMIT));
- RoomSQLiteQuery.acquire("dsadsa", RoomSQLiteQuery.POOL_LIMIT + 1).release();
- assertThat(RoomSQLiteQuery.sQueryPool.size(), is(RoomSQLiteQuery.DESIRED_POOL_SIZE));
- Iterator<RoomSQLiteQuery> itr = RoomSQLiteQuery.sQueryPool.values().iterator();
- for (int i = 0; i < RoomSQLiteQuery.DESIRED_POOL_SIZE; i++) {
- assertThat(itr.next().mCapacity, is(i));
- }
- }
-}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/SharedSQLiteStatementTest.java b/room/runtime/src/test/java/android/arch/persistence/room/SharedSQLiteStatementTest.java
deleted file mode 100644
index 4e715e9..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/SharedSQLiteStatementTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2016 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 static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.arch.persistence.db.SupportSQLiteStatement;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
-
-@RunWith(JUnit4.class)
-public class SharedSQLiteStatementTest {
- private SharedSQLiteStatement mSharedStmt;
- RoomDatabase mDb;
- @Before
- public void init() {
- mDb = mock(RoomDatabase.class);
- when(mDb.compileStatement(anyString())).thenAnswer(new Answer<SupportSQLiteStatement>() {
-
- @Override
- public SupportSQLiteStatement answer(InvocationOnMock invocation) throws Throwable {
- return mock(SupportSQLiteStatement.class);
- }
- });
- when(mDb.getInvalidationTracker()).thenReturn(mock(InvalidationTracker.class));
- mSharedStmt = new SharedSQLiteStatement(mDb) {
- @Override
- protected String createQuery() {
- return "foo";
- }
- };
- }
-
- @Test
- public void checkMainThread() {
- mSharedStmt.acquire();
- verify(mDb).assertNotMainThread();
- }
-
- @Test
- public void basic() {
- assertThat(mSharedStmt.acquire(), notNullValue());
- }
-
- @Test
- public void getTwiceWithoutReleasing() {
- SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
- SupportSQLiteStatement stmt2 = mSharedStmt.acquire();
- assertThat(stmt1, notNullValue());
- assertThat(stmt2, notNullValue());
- assertThat(stmt1, is(not(stmt2)));
- }
-
- @Test
- public void getTwiceWithReleasing() {
- SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
- mSharedStmt.release(stmt1);
- SupportSQLiteStatement stmt2 = mSharedStmt.acquire();
- assertThat(stmt1, notNullValue());
- assertThat(stmt1, is(stmt2));
- }
-
- @Test
- public void getFromAnotherThreadWhileHolding() throws ExecutionException, InterruptedException {
- SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
- FutureTask<SupportSQLiteStatement> task = new FutureTask<>(
- new Callable<SupportSQLiteStatement>() {
- @Override
- public SupportSQLiteStatement call() throws Exception {
- return mSharedStmt.acquire();
- }
- });
- new Thread(task).run();
- SupportSQLiteStatement stmt2 = task.get();
- assertThat(stmt1, notNullValue());
- assertThat(stmt2, notNullValue());
- assertThat(stmt1, is(not(stmt2)));
- }
-
- @Test
- public void getFromAnotherThreadAfterReleasing() throws ExecutionException,
- InterruptedException {
- SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
- mSharedStmt.release(stmt1);
- FutureTask<SupportSQLiteStatement> task = new FutureTask<>(
- new Callable<SupportSQLiteStatement>() {
- @Override
- public SupportSQLiteStatement call() throws Exception {
- return mSharedStmt.acquire();
- }
- });
- new Thread(task).run();
- SupportSQLiteStatement stmt2 = task.get();
- assertThat(stmt1, notNullValue());
- assertThat(stmt1, is(stmt2));
- }
-}
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/util/StringUtilTest.java b/room/runtime/src/test/java/android/arch/persistence/room/util/StringUtilTest.java
deleted file mode 100644
index c10fab6..0000000
--- a/room/runtime/src/test/java/android/arch/persistence/room/util/StringUtilTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 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.util;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import static java.util.Arrays.asList;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Collections;
-
-@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
-@RunWith(JUnit4.class)
-public class StringUtilTest {
- @Test
- public void testEmpty() {
- assertThat(StringUtil.splitToIntList(""), is(Collections.<Integer>emptyList()));
- assertThat(StringUtil.joinIntoString(Collections.<Integer>emptyList()), is(""));
- }
-
- @Test
- public void testNull() {
- assertThat(StringUtil.splitToIntList(null), nullValue());
- assertThat(StringUtil.joinIntoString(null), nullValue());
- }
-
- @Test
- public void testSingle() {
- assertThat(StringUtil.splitToIntList("4"), is(asList(4)));
- assertThat(StringUtil.joinIntoString(asList(4)), is("4"));
- }
-
- @Test
- public void testMultiple() {
- assertThat(StringUtil.splitToIntList("4,5"), is(asList(4, 5)));
- assertThat(StringUtil.joinIntoString(asList(4, 5)), is("4,5"));
- }
-
- @Test
- public void testNegative() {
- assertThat(StringUtil.splitToIntList("-4,-5,6,-7"), is(asList(-4, -5, 6, -7)));
- assertThat(StringUtil.joinIntoString(asList(-4, -5, 6, -7)), is("-4,-5,6,-7"));
- }
-
- @Test
- public void ignoreMalformed() {
- assertThat(StringUtil.splitToIntList("-4,a,5,7"), is(asList(-4, 5, 7)));
- }
-}
diff --git a/room/runtime/src/test/java/androidx/room/BuilderTest.java b/room/runtime/src/test/java/androidx/room/BuilderTest.java
new file mode 100644
index 0000000..cd425cd
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/BuilderTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+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 static org.mockito.Mockito.mock;
+
+import static java.util.Arrays.asList;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class BuilderTest {
+ @Test(expected = IllegalArgumentException.class)
+ public void nullContext() {
+ //noinspection ConstantConditions
+ Room.databaseBuilder(null, RoomDatabase.class, "bla").build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void nullContext2() {
+ //noinspection ConstantConditions
+ Room.inMemoryDatabaseBuilder(null, RoomDatabase.class).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void nullName() {
+ //noinspection ConstantConditions
+ Room.databaseBuilder(mock(Context.class), RoomDatabase.class, null).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void emptyName() {
+ Room.databaseBuilder(mock(Context.class), RoomDatabase.class, " ").build();
+ }
+
+ @Test
+ public void migration() {
+ Migration m1 = new EmptyMigration(0, 1);
+ Migration m2 = new EmptyMigration(1, 2);
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .addMigrations(m1, m2).build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+ assertThat(migrations.findMigrationPath(0, 1), is(asList(m1)));
+ assertThat(migrations.findMigrationPath(1, 2), is(asList(m2)));
+ assertThat(migrations.findMigrationPath(0, 2), is(asList(m1, m2)));
+ assertThat(migrations.findMigrationPath(2, 0), CoreMatchers.<List<Migration>>nullValue());
+ assertThat(migrations.findMigrationPath(0, 3), CoreMatchers.<List<Migration>>nullValue());
+ }
+
+ @Test
+ public void migrationOverride() {
+ Migration m1 = new EmptyMigration(0, 1);
+ Migration m2 = new EmptyMigration(1, 2);
+ Migration m3 = new EmptyMigration(0, 1);
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .addMigrations(m1, m2, m3).build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+ assertThat(migrations.findMigrationPath(0, 1), is(asList(m3)));
+ assertThat(migrations.findMigrationPath(1, 2), is(asList(m2)));
+ assertThat(migrations.findMigrationPath(0, 3), CoreMatchers.<List<Migration>>nullValue());
+ }
+
+ @Test
+ public void migrationJump() {
+ Migration m1 = new EmptyMigration(0, 1);
+ Migration m2 = new EmptyMigration(1, 2);
+ Migration m3 = new EmptyMigration(2, 3);
+ Migration m4 = new EmptyMigration(0, 3);
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .addMigrations(m1, m2, m3, m4).build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+ assertThat(migrations.findMigrationPath(0, 3), is(asList(m4)));
+ assertThat(migrations.findMigrationPath(1, 3), is(asList(m2, m3)));
+ }
+
+ @Test
+ public void migrationDowngrade() {
+ Migration m1_2 = new EmptyMigration(1, 2);
+ Migration m2_3 = new EmptyMigration(2, 3);
+ Migration m3_4 = new EmptyMigration(3, 4);
+ Migration m3_2 = new EmptyMigration(3, 2);
+ Migration m2_1 = new EmptyMigration(2, 1);
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .addMigrations(m1_2, m2_3, m3_4, m3_2, m2_1).build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+ assertThat(migrations.findMigrationPath(3, 2), is(asList(m3_2)));
+ assertThat(migrations.findMigrationPath(3, 1), is(asList(m3_2, m2_1)));
+ }
+
+ @Test
+ public void skipMigration() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigration()
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.requireMigration, is(false));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_calledOnce_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2).build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_calledTwice_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2)
+ .fallbackToDestructiveMigrationFrom(3, 4)
+ .build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ assertThat(config.isMigrationRequiredFrom(3), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestructiveCalled_alwaysReturnsFalse() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigration()
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(false));
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(5), is(false));
+ assertThat(config.isMigrationRequiredFrom(12), is(false));
+ assertThat(config.isMigrationRequiredFrom(132), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_byDefault_alwaysReturnsTrue() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(true));
+ assertThat(config.isMigrationRequiredFrom(1), is(true));
+ assertThat(config.isMigrationRequiredFrom(5), is(true));
+ assertThat(config.isMigrationRequiredFrom(12), is(true));
+ assertThat(config.isMigrationRequiredFrom(132), is(true));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_falseForProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ assertThat(config.isMigrationRequiredFrom(81), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_trueForNonProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(2), is(true));
+ assertThat(config.isMigrationRequiredFrom(3), is(true));
+ assertThat(config.isMigrationRequiredFrom(73), is(true));
+ }
+
+ @Test
+ public void createBasic() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+ assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config, notNullValue());
+ assertThat(config.context, is(context));
+ assertThat(config.name, is(nullValue()));
+ assertThat(config.allowMainThreadQueries, is(false));
+ assertThat(config.journalMode, is(RoomDatabase.JournalMode.TRUNCATE));
+ assertThat(config.sqliteOpenHelperFactory,
+ instanceOf(FrameworkSQLiteOpenHelperFactory.class));
+ }
+
+ @Test
+ public void createAllowMainThread() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .allowMainThreadQueries()
+ .build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.allowMainThreadQueries, is(true));
+ }
+
+ @Test
+ public void createWriteAheadLogging() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "foo")
+ .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING).build();
+ assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.journalMode, is(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING));
+ }
+
+ @Test
+ public void createWithFactoryAndVersion() {
+ Context context = mock(Context.class);
+ SupportSQLiteOpenHelper.Factory factory = mock(SupportSQLiteOpenHelper.Factory.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .openHelperFactory(factory)
+ .build();
+ assertThat(db, instanceOf(BuilderTest_TestDatabase_Impl.class));
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config, notNullValue());
+ assertThat(config.sqliteOpenHelperFactory, is(factory));
+ }
+
+ abstract static class TestDatabase extends RoomDatabase {
+ }
+
+ static class EmptyMigration extends Migration {
+ EmptyMigration(int start, int end) {
+ super(start, end);
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ }
+ }
+
+}
diff --git a/room/runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.java b/room/runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.java
new file mode 100644
index 0000000..d796bdc
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/BuilderTest_TestDatabase_Impl.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 androidx.room;
+
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+public class BuilderTest_TestDatabase_Impl extends BuilderTest.TestDatabase {
+ DatabaseConfiguration mConfig;
+ @Override
+ public void init(DatabaseConfiguration configuration) {
+ super.init(configuration);
+ mConfig = configuration;
+ }
+
+ @Override
+ protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
+ return null;
+ }
+
+ @Override
+ protected InvalidationTracker createInvalidationTracker() {
+ return null;
+ }
+
+ @Override
+ public void clearAllTables() {
+ }
+}
diff --git a/room/runtime/src/test/java/androidx/room/InvalidationTrackerTest.java b/room/runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
new file mode 100644
index 0000000..f4ffd24
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
@@ -0,0 +1,340 @@
+/*
+ * 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 androidx.room;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsCollectionContaining.hasItem;
+import static org.hamcrest.core.IsCollectionContaining.hasItems;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.executor.JunitTaskExecutorRule;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
+
+@RunWith(JUnit4.class)
+public class InvalidationTrackerTest {
+ private InvalidationTracker mTracker;
+ private RoomDatabase mRoomDatabase;
+ private SupportSQLiteOpenHelper mOpenHelper;
+ @Rule
+ public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1, true);
+
+ @Before
+ public void setup() {
+ mRoomDatabase = mock(RoomDatabase.class);
+ SupportSQLiteDatabase sqliteDb = mock(SupportSQLiteDatabase.class);
+ final SupportSQLiteStatement statement = mock(SupportSQLiteStatement.class);
+ mOpenHelper = mock(SupportSQLiteOpenHelper.class);
+
+ doReturn(statement).when(sqliteDb).compileStatement(eq(InvalidationTracker.CLEANUP_SQL));
+ doReturn(sqliteDb).when(mOpenHelper).getWritableDatabase();
+ doReturn(true).when(mRoomDatabase).isOpen();
+ ReentrantLock closeLock = new ReentrantLock();
+ doReturn(closeLock).when(mRoomDatabase).getCloseLock();
+ //noinspection ResultOfMethodCallIgnored
+ doReturn(mOpenHelper).when(mRoomDatabase).getOpenHelper();
+
+ mTracker = new InvalidationTracker(mRoomDatabase, "a", "B", "i");
+ mTracker.internalInit(sqliteDb);
+ }
+
+ @Before
+ public void setLocale() {
+ Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+ }
+
+ @After
+ public void unsetLocale() {
+ Locale.setDefault(Locale.US);
+ }
+
+ @Test
+ public void tableIds() {
+ assertThat(mTracker.mTableIdLookup.get("a"), is(0));
+ assertThat(mTracker.mTableIdLookup.get("b"), is(1));
+ }
+
+ @Test
+ public void testWeak() throws InterruptedException {
+ final AtomicInteger data = new AtomicInteger(0);
+ InvalidationTracker.Observer observer = new InvalidationTracker.Observer("a") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ data.incrementAndGet();
+ }
+ };
+ mTracker.addWeakObserver(observer);
+ setVersions(1, 0);
+ refreshSync();
+ assertThat(data.get(), is(1));
+ observer = null;
+ forceGc();
+ setVersions(2, 0);
+ refreshSync();
+ assertThat(data.get(), is(1));
+ }
+
+ @Test
+ public void addRemoveObserver() throws Exception {
+ InvalidationTracker.Observer observer = new LatchObserver(1, "a");
+ mTracker.addObserver(observer);
+ assertThat(mTracker.mObserverMap.size(), is(1));
+ mTracker.removeObserver(new LatchObserver(1, "a"));
+ assertThat(mTracker.mObserverMap.size(), is(1));
+ mTracker.removeObserver(observer);
+ assertThat(mTracker.mObserverMap.size(), is(0));
+ }
+
+ private void drainTasks() throws InterruptedException {
+ mTaskExecutorRule.drainTasks(200);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void badObserver() {
+ InvalidationTracker.Observer observer = new LatchObserver(1, "x");
+ mTracker.addObserver(observer);
+ }
+
+ @Test
+ public void refreshReadValues() throws Exception {
+ setVersions(1, 0, 2, 1);
+ refreshSync();
+ assertThat(mTracker.mTableVersions, is(new long[]{1, 2, 0}));
+
+ setVersions(3, 1);
+ refreshSync();
+ assertThat(mTracker.mTableVersions, is(new long[]{1, 3, 0}));
+
+ setVersions(7, 0);
+ refreshSync();
+ assertThat(mTracker.mTableVersions, is(new long[]{7, 3, 0}));
+
+ refreshSync();
+ assertThat(mTracker.mTableVersions, is(new long[]{7, 3, 0}));
+ }
+
+ private void refreshSync() throws InterruptedException {
+ mTracker.refreshVersionsAsync();
+ drainTasks();
+ }
+
+ @Test
+ public void refreshCheckTasks() throws Exception {
+ when(mRoomDatabase.query(anyString(), any(Object[].class)))
+ .thenReturn(mock(Cursor.class));
+ mTracker.refreshVersionsAsync();
+ mTracker.refreshVersionsAsync();
+ verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
+ drainTasks();
+
+ reset(mTaskExecutorRule.getTaskExecutor());
+ mTracker.refreshVersionsAsync();
+ verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
+ }
+
+ @Test
+ public void observe1Table() throws Exception {
+ LatchObserver observer = new LatchObserver(1, "a");
+ mTracker.addObserver(observer);
+ setVersions(1, 0, 2, 1);
+ refreshSync();
+ assertThat(observer.await(), is(true));
+ assertThat(observer.getInvalidatedTables().size(), is(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("a"));
+
+ setVersions(3, 1);
+ observer.reset(1);
+ refreshSync();
+ assertThat(observer.await(), is(false));
+
+ setVersions(4, 0);
+ refreshSync();
+ assertThat(observer.await(), is(true));
+ assertThat(observer.getInvalidatedTables().size(), is(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("a"));
+ }
+
+ @Test
+ public void observe2Tables() throws Exception {
+ LatchObserver observer = new LatchObserver(1, "A", "B");
+ mTracker.addObserver(observer);
+ setVersions(1, 0, 2, 1);
+ refreshSync();
+ assertThat(observer.await(), is(true));
+ assertThat(observer.getInvalidatedTables().size(), is(2));
+ assertThat(observer.getInvalidatedTables(), hasItems("A", "B"));
+
+ setVersions(3, 1);
+ observer.reset(1);
+ refreshSync();
+ assertThat(observer.await(), is(true));
+ assertThat(observer.getInvalidatedTables().size(), is(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("B"));
+
+ setVersions(4, 0);
+ observer.reset(1);
+ refreshSync();
+ assertThat(observer.await(), is(true));
+ assertThat(observer.getInvalidatedTables().size(), is(1));
+ assertThat(observer.getInvalidatedTables(), hasItem("A"));
+
+ observer.reset(1);
+ refreshSync();
+ assertThat(observer.await(), is(false));
+ }
+
+ @Test
+ public void locale() {
+ LatchObserver observer = new LatchObserver(1, "I");
+ mTracker.addObserver(observer);
+ }
+
+ @Test
+ public void closedDb() {
+ doReturn(false).when(mRoomDatabase).isOpen();
+ doThrow(new IllegalStateException("foo")).when(mOpenHelper).getWritableDatabase();
+ mTracker.addObserver(new LatchObserver(1, "a", "b"));
+ mTracker.mRefreshRunnable.run();
+ }
+
+ // @Test - disabled due to flakiness b/65257997
+ public void closedDbAfterOpen() throws InterruptedException {
+ setVersions(3, 1);
+ mTracker.addObserver(new LatchObserver(1, "a", "b"));
+ mTracker.syncTriggers();
+ mTracker.mRefreshRunnable.run();
+ doThrow(new SQLiteException("foo")).when(mRoomDatabase).query(
+ Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
+ any(Object[].class));
+ mTracker.mPendingRefresh.set(true);
+ mTracker.mRefreshRunnable.run();
+ }
+
+ /**
+ * Key value pairs of VERSION, TABLE_ID
+ */
+ private void setVersions(int... keyValuePairs) throws InterruptedException {
+ // mockito does not like multi-threaded access so before setting versions, make sure we
+ // sync background tasks.
+ drainTasks();
+ Cursor cursor = createCursorWithValues(keyValuePairs);
+ doReturn(cursor).when(mRoomDatabase).query(
+ Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
+ any(Object[].class)
+ );
+ }
+
+ private Cursor createCursorWithValues(final int... keyValuePairs) {
+ Cursor cursor = mock(Cursor.class);
+ final AtomicInteger index = new AtomicInteger(-2);
+ when(cursor.moveToNext()).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ return index.addAndGet(2) < keyValuePairs.length;
+ }
+ });
+ Answer<Integer> intAnswer = new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ return keyValuePairs[index.intValue() + (Integer) invocation.getArguments()[0]];
+ }
+ };
+ Answer<Long> longAnswer = new Answer<Long>() {
+ @Override
+ public Long answer(InvocationOnMock invocation) throws Throwable {
+ return (long) keyValuePairs[index.intValue()
+ + (Integer) invocation.getArguments()[0]];
+ }
+ };
+ when(cursor.getInt(anyInt())).thenAnswer(intAnswer);
+ when(cursor.getLong(anyInt())).thenAnswer(longAnswer);
+ return cursor;
+ }
+
+ static class LatchObserver extends InvalidationTracker.Observer {
+ private CountDownLatch mLatch;
+ private Set<String> mInvalidatedTables;
+
+ LatchObserver(int count, String... tableNames) {
+ super(tableNames);
+ mLatch = new CountDownLatch(count);
+ }
+
+ boolean await() throws InterruptedException {
+ return mLatch.await(3, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ mInvalidatedTables = tables;
+ mLatch.countDown();
+ }
+
+ void reset(@SuppressWarnings("SameParameterValue") int count) {
+ mInvalidatedTables = null;
+ mLatch = new CountDownLatch(count);
+ }
+
+ Set<String> getInvalidatedTables() {
+ return mInvalidatedTables;
+ }
+ }
+
+ private static void forceGc() {
+ // Use a random index in the list to detect the garbage collection each time because
+ // .get() may accidentally trigger a strong reference during collection.
+ ArrayList<WeakReference<byte[]>> leak = new ArrayList<>();
+ do {
+ WeakReference<byte[]> arr = new WeakReference<>(new byte[100]);
+ leak.add(arr);
+ } while (leak.get((int) (Math.random() * leak.size())).get() != null);
+ }
+}
diff --git a/room/runtime/src/test/java/androidx/room/ObservedTableTrackerTest.java b/room/runtime/src/test/java/androidx/room/ObservedTableTrackerTest.java
new file mode 100644
index 0000000..224815f
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/ObservedTableTrackerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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 androidx.room;
+
+
+import static androidx.room.InvalidationTracker.ObservedTableTracker.ADD;
+import static androidx.room.InvalidationTracker.ObservedTableTracker.NO_OP;
+import static androidx.room.InvalidationTracker.ObservedTableTracker.REMOVE;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class ObservedTableTrackerTest {
+ private static final int TABLE_COUNT = 5;
+ private InvalidationTracker.ObservedTableTracker mTracker;
+
+ @Before
+ public void setup() {
+ mTracker = new InvalidationTracker.ObservedTableTracker(TABLE_COUNT);
+ }
+
+ @Test
+ public void basicAdd() {
+ mTracker.onAdded(2, 3);
+ assertThat(mTracker.getTablesToSync(), is(createResponse(2, ADD, 3, ADD)));
+ }
+
+ @Test
+ public void basicRemove() {
+ initState(2, 3);
+ mTracker.onRemoved(3);
+ assertThat(mTracker.getTablesToSync(), is(createResponse(3, REMOVE)));
+ }
+
+ @Test
+ public void noChange() {
+ initState(1, 3);
+ mTracker.onAdded(3);
+ assertThat(mTracker.getTablesToSync(), is(nullValue()));
+ }
+
+ @Test
+ public void returnNullUntilSync() {
+ initState(1, 3);
+ mTracker.onAdded(4);
+ assertThat(mTracker.getTablesToSync(), is(createResponse(4, ADD)));
+ mTracker.onAdded(0);
+ assertThat(mTracker.getTablesToSync(), is(nullValue()));
+ mTracker.onSyncCompleted();
+ assertThat(mTracker.getTablesToSync(), is(createResponse(0, ADD)));
+ }
+
+ @Test
+ public void multipleAdditionsDeletions() {
+ initState(2, 4);
+ mTracker.onAdded(2);
+ assertThat(mTracker.getTablesToSync(), is(nullValue()));
+ mTracker.onAdded(2, 4);
+ assertThat(mTracker.getTablesToSync(), is(nullValue()));
+ mTracker.onRemoved(2);
+ assertThat(mTracker.getTablesToSync(), is(nullValue()));
+ mTracker.onRemoved(2, 4);
+ assertThat(mTracker.getTablesToSync(), is(nullValue()));
+ mTracker.onAdded(1, 3);
+ mTracker.onRemoved(2, 4);
+ assertThat(mTracker.getTablesToSync(), is(
+ createResponse(1, ADD, 2, REMOVE, 3, ADD, 4, REMOVE)));
+ }
+
+ private void initState(int... tableIds) {
+ mTracker.onAdded(tableIds);
+ mTracker.getTablesToSync();
+ mTracker.onSyncCompleted();
+ }
+
+ private static int[] createResponse(int... tuples) {
+ int[] result = new int[TABLE_COUNT];
+ Arrays.fill(result, NO_OP);
+ for (int i = 0; i < tuples.length; i += 2) {
+ result[tuples[i]] = tuples[i + 1];
+ }
+ return result;
+ }
+}
diff --git a/room/runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java b/room/runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
new file mode 100644
index 0000000..9a70c2e
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/RoomSQLiteQueryTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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 androidx.room;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import androidx.sqlite.db.SupportSQLiteProgram;
+
+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.Iterator;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class RoomSQLiteQueryTest {
+ @Before
+ public void clear() {
+ RoomSQLiteQuery.sQueryPool.clear();
+ }
+
+ @Test
+ public void acquireBasic() {
+ RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
+ assertThat(query.getSql(), is("abc"));
+ assertThat(query.mArgCount, is(3));
+ assertThat(query.mBlobBindings.length, is(4));
+ assertThat(query.mLongBindings.length, is(4));
+ assertThat(query.mStringBindings.length, is(4));
+ assertThat(query.mDoubleBindings.length, is(4));
+ }
+
+ @Test
+ public void acquireSameSizeAgain() {
+ RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
+ query.release();
+ assertThat(RoomSQLiteQuery.acquire("blah", 3), sameInstance(query));
+ }
+
+ @Test
+ public void acquireSameSizeWithoutRelease() {
+ RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
+ assertThat(RoomSQLiteQuery.acquire("fda", 3), not(sameInstance(query)));
+ }
+
+ @Test
+ public void bindings() {
+ RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 6);
+ byte[] myBlob = new byte[3];
+ long myLong = 3L;
+ double myDouble = 7.0;
+ String myString = "ss";
+ query.bindBlob(1, myBlob);
+ query.bindLong(2, myLong);
+ query.bindNull(3);
+ query.bindDouble(4, myDouble);
+ query.bindString(5, myString);
+ query.bindNull(6);
+ SupportSQLiteProgram program = mock(SupportSQLiteProgram.class);
+ query.bindTo(program);
+
+ verify(program).bindBlob(1, myBlob);
+ verify(program).bindLong(2, myLong);
+ verify(program).bindNull(3);
+ verify(program).bindDouble(4, myDouble);
+ verify(program).bindString(5, myString);
+ verify(program).bindNull(6);
+ }
+
+ @Test
+ public void dontKeepSameSizeTwice() {
+ RoomSQLiteQuery query1 = RoomSQLiteQuery.acquire("abc", 3);
+ RoomSQLiteQuery query2 = RoomSQLiteQuery.acquire("zx", 3);
+ RoomSQLiteQuery query3 = RoomSQLiteQuery.acquire("qw", 0);
+
+ query1.release();
+ query2.release();
+ assertThat(RoomSQLiteQuery.sQueryPool.size(), is(1));
+
+ query3.release();
+ assertThat(RoomSQLiteQuery.sQueryPool.size(), is(2));
+ }
+
+ @Test
+ public void returnExistingForSmallerSize() {
+ RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
+ query.release();
+ assertThat(RoomSQLiteQuery.acquire("dsa", 2), sameInstance(query));
+ }
+
+ @Test
+ public void returnNewForBigger() {
+ RoomSQLiteQuery query = RoomSQLiteQuery.acquire("abc", 3);
+ query.release();
+ assertThat(RoomSQLiteQuery.acquire("dsa", 4), not(sameInstance(query)));
+ }
+
+ @Test
+ public void pruneCache() {
+ for (int i = 0; i < RoomSQLiteQuery.POOL_LIMIT; i++) {
+ RoomSQLiteQuery.acquire("dsdsa", i).release();
+ }
+ pruneCacheTest();
+ }
+
+ @Test
+ public void pruneCacheReverseInsertion() {
+ List<RoomSQLiteQuery> queries = new ArrayList<>();
+ for (int i = RoomSQLiteQuery.POOL_LIMIT - 1; i >= 0; i--) {
+ queries.add(RoomSQLiteQuery.acquire("dsdsa", i));
+ }
+ for (RoomSQLiteQuery query : queries) {
+ query.release();
+ }
+ pruneCacheTest();
+ }
+
+ private void pruneCacheTest() {
+ assertThat(RoomSQLiteQuery.sQueryPool.size(), is(RoomSQLiteQuery.POOL_LIMIT));
+ RoomSQLiteQuery.acquire("dsadsa", RoomSQLiteQuery.POOL_LIMIT + 1).release();
+ assertThat(RoomSQLiteQuery.sQueryPool.size(), is(RoomSQLiteQuery.DESIRED_POOL_SIZE));
+ Iterator<RoomSQLiteQuery> itr = RoomSQLiteQuery.sQueryPool.values().iterator();
+ for (int i = 0; i < RoomSQLiteQuery.DESIRED_POOL_SIZE; i++) {
+ assertThat(itr.next().mCapacity, is(i));
+ }
+ }
+}
diff --git a/room/runtime/src/test/java/androidx/room/SharedSQLiteStatementTest.java b/room/runtime/src/test/java/androidx/room/SharedSQLiteStatementTest.java
new file mode 100644
index 0000000..4dad7c2
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/SharedSQLiteStatementTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 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 androidx.room;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+@RunWith(JUnit4.class)
+public class SharedSQLiteStatementTest {
+ private SharedSQLiteStatement mSharedStmt;
+ RoomDatabase mDb;
+ @Before
+ public void init() {
+ mDb = mock(RoomDatabase.class);
+ when(mDb.compileStatement(anyString())).thenAnswer(new Answer<SupportSQLiteStatement>() {
+
+ @Override
+ public SupportSQLiteStatement answer(InvocationOnMock invocation) throws Throwable {
+ return mock(SupportSQLiteStatement.class);
+ }
+ });
+ when(mDb.getInvalidationTracker()).thenReturn(mock(InvalidationTracker.class));
+ mSharedStmt = new SharedSQLiteStatement(mDb) {
+ @Override
+ protected String createQuery() {
+ return "foo";
+ }
+ };
+ }
+
+ @Test
+ public void checkMainThread() {
+ mSharedStmt.acquire();
+ verify(mDb).assertNotMainThread();
+ }
+
+ @Test
+ public void basic() {
+ assertThat(mSharedStmt.acquire(), notNullValue());
+ }
+
+ @Test
+ public void getTwiceWithoutReleasing() {
+ SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
+ SupportSQLiteStatement stmt2 = mSharedStmt.acquire();
+ assertThat(stmt1, notNullValue());
+ assertThat(stmt2, notNullValue());
+ assertThat(stmt1, is(not(stmt2)));
+ }
+
+ @Test
+ public void getTwiceWithReleasing() {
+ SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
+ mSharedStmt.release(stmt1);
+ SupportSQLiteStatement stmt2 = mSharedStmt.acquire();
+ assertThat(stmt1, notNullValue());
+ assertThat(stmt1, is(stmt2));
+ }
+
+ @Test
+ public void getFromAnotherThreadWhileHolding() throws ExecutionException, InterruptedException {
+ SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
+ FutureTask<SupportSQLiteStatement> task = new FutureTask<>(
+ new Callable<SupportSQLiteStatement>() {
+ @Override
+ public SupportSQLiteStatement call() throws Exception {
+ return mSharedStmt.acquire();
+ }
+ });
+ new Thread(task).run();
+ SupportSQLiteStatement stmt2 = task.get();
+ assertThat(stmt1, notNullValue());
+ assertThat(stmt2, notNullValue());
+ assertThat(stmt1, is(not(stmt2)));
+ }
+
+ @Test
+ public void getFromAnotherThreadAfterReleasing() throws ExecutionException,
+ InterruptedException {
+ SupportSQLiteStatement stmt1 = mSharedStmt.acquire();
+ mSharedStmt.release(stmt1);
+ FutureTask<SupportSQLiteStatement> task = new FutureTask<>(
+ new Callable<SupportSQLiteStatement>() {
+ @Override
+ public SupportSQLiteStatement call() throws Exception {
+ return mSharedStmt.acquire();
+ }
+ });
+ new Thread(task).run();
+ SupportSQLiteStatement stmt2 = task.get();
+ assertThat(stmt1, notNullValue());
+ assertThat(stmt1, is(stmt2));
+ }
+}
diff --git a/room/runtime/src/test/java/androidx/room/util/StringUtilTest.java b/room/runtime/src/test/java/androidx/room/util/StringUtilTest.java
new file mode 100644
index 0000000..7ea316c
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/util/StringUtilTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 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 androidx.room.util;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class StringUtilTest {
+ @Test
+ public void testEmpty() {
+ assertThat(StringUtil.splitToIntList(""), is(Collections.<Integer>emptyList()));
+ assertThat(StringUtil.joinIntoString(Collections.<Integer>emptyList()), is(""));
+ }
+
+ @Test
+ public void testNull() {
+ assertThat(StringUtil.splitToIntList(null), nullValue());
+ assertThat(StringUtil.joinIntoString(null), nullValue());
+ }
+
+ @Test
+ public void testSingle() {
+ assertThat(StringUtil.splitToIntList("4"), is(asList(4)));
+ assertThat(StringUtil.joinIntoString(asList(4)), is("4"));
+ }
+
+ @Test
+ public void testMultiple() {
+ assertThat(StringUtil.splitToIntList("4,5"), is(asList(4, 5)));
+ assertThat(StringUtil.joinIntoString(asList(4, 5)), is("4,5"));
+ }
+
+ @Test
+ public void testNegative() {
+ assertThat(StringUtil.splitToIntList("-4,-5,6,-7"), is(asList(-4, -5, 6, -7)));
+ assertThat(StringUtil.joinIntoString(asList(-4, -5, 6, -7)), is("-4,-5,6,-7"));
+ }
+
+ @Test
+ public void ignoreMalformed() {
+ assertThat(StringUtil.splitToIntList("-4,a,5,7"), is(asList(-4, 5, 7)));
+ }
+}
diff --git a/room/rxjava2/api/current.txt b/room/rxjava2/api/current.txt
index a7cffd3..be690d9 100644
--- a/room/rxjava2/api/current.txt
+++ b/room/rxjava2/api/current.txt
@@ -1,4 +1,4 @@
-package android.arch.persistence.room {
+package androidx.room {
public class EmptyResultSetException extends java.lang.RuntimeException {
ctor public EmptyResultSetException(java.lang.String);
@@ -6,7 +6,7 @@
public class RxRoom {
ctor public RxRoom();
- method public static io.reactivex.Flowable<java.lang.Object> createFlowable(android.arch.persistence.room.RoomDatabase, java.lang.String...);
+ method public static io.reactivex.Flowable<java.lang.Object> createFlowable(androidx.room.RoomDatabase, java.lang.String...);
field public static final java.lang.Object NOTHING;
}
diff --git a/room/rxjava2/api/1.0.0.txt b/room/rxjava2/api_legacy/1.0.0.txt
similarity index 100%
rename from room/rxjava2/api/1.0.0.txt
rename to room/rxjava2/api_legacy/1.0.0.txt
diff --git a/room/rxjava2/api/1.0.0.txt b/room/rxjava2/api_legacy/1.1.0.txt
similarity index 100%
copy from room/rxjava2/api/1.0.0.txt
copy to room/rxjava2/api_legacy/1.1.0.txt
diff --git a/room/rxjava2/api/1.0.0.txt b/room/rxjava2/api_legacy/current.txt
similarity index 100%
copy from room/rxjava2/api/1.0.0.txt
copy to room/rxjava2/api_legacy/current.txt
diff --git a/room/rxjava2/build.gradle b/room/rxjava2/build.gradle
index 8dcaa6b..1457f79 100644
--- a/room/rxjava2/build.gradle
+++ b/room/rxjava2/build.gradle
@@ -24,9 +24,9 @@
}
dependencies {
- api(project(":room:common"))
- api(project(":room:runtime"))
- api(project(":arch:runtime"))
+ api(project(":room:room-common"))
+ api(project(":room:room-runtime"))
+ api(project(":arch:core-runtime"))
api(SUPPORT_CORE_UTILS, libs.support_exclude_config)
api(RX_JAVA)
testImplementation(JUNIT)
diff --git a/room/rxjava2/src/main/AndroidManifest.xml b/room/rxjava2/src/main/AndroidManifest.xml
index 33279c6..dc3a472 100644
--- a/room/rxjava2/src/main/AndroidManifest.xml
+++ b/room/rxjava2/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.room.rxjava2">
+ package="androidx.room.rxjava2">
</manifest>
diff --git a/room/rxjava2/src/main/java/android/arch/persistence/room/EmptyResultSetException.java b/room/rxjava2/src/main/java/android/arch/persistence/room/EmptyResultSetException.java
deleted file mode 100644
index 0f2d281..0000000
--- a/room/rxjava2/src/main/java/android/arch/persistence/room/EmptyResultSetException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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;
-
-/**
- * Thrown by Room when the query needs to return a result (e.g. in a Single<T> query) but the
- * returned result set from the database is empty.
- */
-public class EmptyResultSetException extends RuntimeException {
- /**
- * Constructs a new EmptyResultSetException with the exception.
- * @param message The SQL query which didn't return any results.
- */
- public EmptyResultSetException(String message) {
- super(message);
- }
-}
diff --git a/room/rxjava2/src/main/java/android/arch/persistence/room/RxRoom.java b/room/rxjava2/src/main/java/android/arch/persistence/room/RxRoom.java
deleted file mode 100644
index 285b3f8..0000000
--- a/room/rxjava2/src/main/java/android/arch/persistence/room/RxRoom.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * 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 android.arch.core.executor.ArchTaskExecutor;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import io.reactivex.BackpressureStrategy;
-import io.reactivex.Flowable;
-import io.reactivex.FlowableEmitter;
-import io.reactivex.FlowableOnSubscribe;
-import io.reactivex.Scheduler;
-import io.reactivex.annotations.NonNull;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.disposables.Disposables;
-import io.reactivex.functions.Action;
-import io.reactivex.functions.Function;
-import io.reactivex.functions.Predicate;
-
-/**
- * Helper class to add RxJava2 support to Room.
- */
-@SuppressWarnings("WeakerAccess")
-public class RxRoom {
- /**
- * Data dispatched by the publisher created by {@link #createFlowable(RoomDatabase, String...)}.
- */
- public static final Object NOTHING = new Object();
-
- /**
- * Creates a {@link Flowable} that emits at least once and also re-emits whenever one of the
- * observed tables is updated.
- * <p>
- * You can easily chain a database operation to downstream of this {@link Flowable} to ensure
- * that it re-runs when database is modified.
- * <p>
- * Since database invalidation is batched, multiple changes in the database may results in just
- * 1 emission.
- *
- * @param database The database instance
- * @param tableNames The list of table names that should be observed
- * @return A {@link Flowable} which emits {@link #NOTHING} when one of the observed tables
- * is modified (also once when the invalidation tracker connection is established).
- */
- public static Flowable<Object> createFlowable(final RoomDatabase database,
- final String... tableNames) {
- return Flowable.create(new FlowableOnSubscribe<Object>() {
- @Override
- public void subscribe(final FlowableEmitter<Object> emitter) throws Exception {
- final InvalidationTracker.Observer observer = new InvalidationTracker.Observer(
- tableNames) {
- @Override
- public void onInvalidated(
- @android.support.annotation.NonNull Set<String> tables) {
- if (!emitter.isCancelled()) {
- emitter.onNext(NOTHING);
- }
- }
- };
- if (!emitter.isCancelled()) {
- database.getInvalidationTracker().addObserver(observer);
- emitter.setDisposable(Disposables.fromAction(new Action() {
- @Override
- public void run() throws Exception {
- database.getInvalidationTracker().removeObserver(observer);
- }
- }));
- }
-
- // emit once to avoid missing any data and also easy chaining
- if (!emitter.isCancelled()) {
- emitter.onNext(NOTHING);
- }
- }
- }, BackpressureStrategy.LATEST);
- }
-
- /**
- * Helper method used by generated code to bind a Callable such that it will be run in
- * our disk io thread and will automatically block null values since RxJava2 does not like null.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static <T> Flowable<T> createFlowable(final RoomDatabase database,
- final String[] tableNames, final Callable<T> callable) {
- return createFlowable(database, tableNames).observeOn(sAppToolkitIOScheduler)
- .map(new Function<Object, Optional<T>>() {
- @Override
- public Optional<T> apply(@NonNull Object o) throws Exception {
- T data = callable.call();
- return new Optional<>(data);
- }
- }).filter(new Predicate<Optional<T>>() {
- @Override
- public boolean test(@NonNull Optional<T> optional) throws Exception {
- return optional.mValue != null;
- }
- }).map(new Function<Optional<T>, T>() {
- @Override
- public T apply(@NonNull Optional<T> optional) throws Exception {
- return optional.mValue;
- }
- });
- }
-
- private static Scheduler sAppToolkitIOScheduler = new Scheduler() {
- @Override
- public Worker createWorker() {
- final AtomicBoolean mDisposed = new AtomicBoolean(false);
- return new Worker() {
- @Override
- public Disposable schedule(@NonNull Runnable run, long delay,
- @NonNull TimeUnit unit) {
- DisposableRunnable disposable = new DisposableRunnable(run, mDisposed);
- ArchTaskExecutor.getInstance().executeOnDiskIO(run);
- return disposable;
- }
-
- @Override
- public void dispose() {
- mDisposed.set(true);
- }
-
- @Override
- public boolean isDisposed() {
- return mDisposed.get();
- }
- };
- }
- };
-
- private static class DisposableRunnable implements Disposable, Runnable {
- private final Runnable mActual;
- private volatile boolean mDisposed = false;
- private final AtomicBoolean mGlobalDisposed;
-
- DisposableRunnable(Runnable actual, AtomicBoolean globalDisposed) {
- mActual = actual;
- mGlobalDisposed = globalDisposed;
- }
-
- @Override
- public void dispose() {
- mDisposed = true;
- }
-
- @Override
- public boolean isDisposed() {
- return mDisposed || mGlobalDisposed.get();
- }
-
- @Override
- public void run() {
- if (!isDisposed()) {
- mActual.run();
- }
- }
- }
-
- static class Optional<T> {
- @Nullable
- final T mValue;
-
- Optional(@Nullable T value) {
- this.mValue = value;
- }
- }
-}
diff --git a/room/rxjava2/src/main/java/androidx/room/EmptyResultSetException.java b/room/rxjava2/src/main/java/androidx/room/EmptyResultSetException.java
new file mode 100644
index 0000000..3081842
--- /dev/null
+++ b/room/rxjava2/src/main/java/androidx/room/EmptyResultSetException.java
@@ -0,0 +1,31 @@
+/*
+ * 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 androidx.room;
+
+/**
+ * Thrown by Room when the query needs to return a result (e.g. in a Single<T> query) but the
+ * returned result set from the database is empty.
+ */
+public class EmptyResultSetException extends RuntimeException {
+ /**
+ * Constructs a new EmptyResultSetException with the exception.
+ * @param message The SQL query which didn't return any results.
+ */
+ public EmptyResultSetException(String message) {
+ super(message);
+ }
+}
diff --git a/room/rxjava2/src/main/java/androidx/room/RxRoom.java b/room/rxjava2/src/main/java/androidx/room/RxRoom.java
new file mode 100644
index 0000000..d2ba8bf
--- /dev/null
+++ b/room/rxjava2/src/main/java/androidx/room/RxRoom.java
@@ -0,0 +1,188 @@
+/*
+ * 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 androidx.room;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+import io.reactivex.FlowableEmitter;
+import io.reactivex.FlowableOnSubscribe;
+import io.reactivex.Scheduler;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.disposables.Disposables;
+import io.reactivex.functions.Action;
+import io.reactivex.functions.Function;
+import io.reactivex.functions.Predicate;
+
+/**
+ * Helper class to add RxJava2 support to Room.
+ */
+@SuppressWarnings("WeakerAccess")
+public class RxRoom {
+ /**
+ * Data dispatched by the publisher created by {@link #createFlowable(RoomDatabase, String...)}.
+ */
+ public static final Object NOTHING = new Object();
+
+ /**
+ * Creates a {@link Flowable} that emits at least once and also re-emits whenever one of the
+ * observed tables is updated.
+ * <p>
+ * You can easily chain a database operation to downstream of this {@link Flowable} to ensure
+ * that it re-runs when database is modified.
+ * <p>
+ * Since database invalidation is batched, multiple changes in the database may results in just
+ * 1 emission.
+ *
+ * @param database The database instance
+ * @param tableNames The list of table names that should be observed
+ * @return A {@link Flowable} which emits {@link #NOTHING} when one of the observed tables
+ * is modified (also once when the invalidation tracker connection is established).
+ */
+ public static Flowable<Object> createFlowable(final RoomDatabase database,
+ final String... tableNames) {
+ return Flowable.create(new FlowableOnSubscribe<Object>() {
+ @Override
+ public void subscribe(final FlowableEmitter<Object> emitter) throws Exception {
+ final InvalidationTracker.Observer observer = new InvalidationTracker.Observer(
+ tableNames) {
+ @Override
+ public void onInvalidated(@androidx.annotation.NonNull Set<String> tables) {
+ if (!emitter.isCancelled()) {
+ emitter.onNext(NOTHING);
+ }
+ }
+ };
+ if (!emitter.isCancelled()) {
+ database.getInvalidationTracker().addObserver(observer);
+ emitter.setDisposable(Disposables.fromAction(new Action() {
+ @Override
+ public void run() throws Exception {
+ database.getInvalidationTracker().removeObserver(observer);
+ }
+ }));
+ }
+
+ // emit once to avoid missing any data and also easy chaining
+ if (!emitter.isCancelled()) {
+ emitter.onNext(NOTHING);
+ }
+ }
+ }, BackpressureStrategy.LATEST);
+ }
+
+ /**
+ * Helper method used by generated code to bind a Callable such that it will be run in
+ * our disk io thread and will automatically block null values since RxJava2 does not like null.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static <T> Flowable<T> createFlowable(final RoomDatabase database,
+ final String[] tableNames, final Callable<T> callable) {
+ return createFlowable(database, tableNames).observeOn(sAppToolkitIOScheduler)
+ .map(new Function<Object, Optional<T>>() {
+ @Override
+ public Optional<T> apply(@NonNull Object o) throws Exception {
+ T data = callable.call();
+ return new Optional<>(data);
+ }
+ }).filter(new Predicate<Optional<T>>() {
+ @Override
+ public boolean test(@NonNull Optional<T> optional) throws Exception {
+ return optional.mValue != null;
+ }
+ }).map(new Function<Optional<T>, T>() {
+ @Override
+ public T apply(@NonNull Optional<T> optional) throws Exception {
+ return optional.mValue;
+ }
+ });
+ }
+
+ private static Scheduler sAppToolkitIOScheduler = new Scheduler() {
+ @Override
+ public Worker createWorker() {
+ final AtomicBoolean mDisposed = new AtomicBoolean(false);
+ return new Worker() {
+ @Override
+ public Disposable schedule(@NonNull Runnable run, long delay,
+ @NonNull TimeUnit unit) {
+ DisposableRunnable disposable = new DisposableRunnable(run, mDisposed);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(run);
+ return disposable;
+ }
+
+ @Override
+ public void dispose() {
+ mDisposed.set(true);
+ }
+
+ @Override
+ public boolean isDisposed() {
+ return mDisposed.get();
+ }
+ };
+ }
+ };
+
+ private static class DisposableRunnable implements Disposable, Runnable {
+ private final Runnable mActual;
+ private volatile boolean mDisposed = false;
+ private final AtomicBoolean mGlobalDisposed;
+
+ DisposableRunnable(Runnable actual, AtomicBoolean globalDisposed) {
+ mActual = actual;
+ mGlobalDisposed = globalDisposed;
+ }
+
+ @Override
+ public void dispose() {
+ mDisposed = true;
+ }
+
+ @Override
+ public boolean isDisposed() {
+ return mDisposed || mGlobalDisposed.get();
+ }
+
+ @Override
+ public void run() {
+ if (!isDisposed()) {
+ mActual.run();
+ }
+ }
+ }
+
+ static class Optional<T> {
+ @Nullable
+ final T mValue;
+
+ Optional(@Nullable T value) {
+ this.mValue = value;
+ }
+ }
+}
diff --git a/room/rxjava2/src/test/java/android/arch/persistence/room/RxRoomTest.java b/room/rxjava2/src/test/java/android/arch/persistence/room/RxRoomTest.java
deleted file mode 100644
index 502eaa1..0000000
--- a/room/rxjava2/src/test/java/android/arch/persistence/room/RxRoomTest.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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 static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.arch.core.executor.JunitTaskExecutorRule;
-
-import org.hamcrest.CoreMatchers;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.atomic.AtomicReference;
-
-import io.reactivex.Flowable;
-import io.reactivex.annotations.NonNull;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.functions.Consumer;
-import io.reactivex.subscribers.TestSubscriber;
-
-@RunWith(JUnit4.class)
-public class RxRoomTest {
- @Rule
- public JunitTaskExecutorRule mExecutor = new JunitTaskExecutorRule(1, false);
- private RoomDatabase mDatabase;
- private InvalidationTracker mInvalidationTracker;
- private List<InvalidationTracker.Observer> mAddedObservers = new ArrayList<>();
-
- @Before
- public void init() {
- mDatabase = mock(RoomDatabase.class);
- mInvalidationTracker = mock(InvalidationTracker.class);
- when(mDatabase.getInvalidationTracker()).thenReturn(mInvalidationTracker);
- doAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- mAddedObservers.add((InvalidationTracker.Observer) invocation.getArguments()[0]);
- return null;
- }
- }).when(mInvalidationTracker).addObserver(any(InvalidationTracker.Observer.class));
- }
-
- @Test
- public void basicAddRemove() {
- Flowable<Object> flowable = RxRoom.createFlowable(mDatabase, "a", "b");
- verify(mInvalidationTracker, never()).addObserver(any(InvalidationTracker.Observer.class));
- Disposable disposable = flowable.subscribe();
- verify(mInvalidationTracker).addObserver(any(InvalidationTracker.Observer.class));
- assertThat(mAddedObservers.size(), CoreMatchers.is(1));
-
- InvalidationTracker.Observer observer = mAddedObservers.get(0);
- disposable.dispose();
-
- verify(mInvalidationTracker).removeObserver(observer);
-
- disposable = flowable.subscribe();
- verify(mInvalidationTracker, times(2))
- .addObserver(any(InvalidationTracker.Observer.class));
- assertThat(mAddedObservers.size(), CoreMatchers.is(2));
- assertThat(mAddedObservers.get(1), CoreMatchers.not(CoreMatchers.sameInstance(observer)));
- InvalidationTracker.Observer observer2 = mAddedObservers.get(1);
- disposable.dispose();
- verify(mInvalidationTracker).removeObserver(observer2);
- }
-
- @Test
- public void basicNotify() throws InterruptedException {
- String[] tables = {"a", "b"};
- Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
- Flowable<Object> flowable = RxRoom.createFlowable(mDatabase, tables);
- CountingConsumer consumer = new CountingConsumer();
- Disposable disposable = flowable.subscribe(consumer);
- assertThat(mAddedObservers.size(), CoreMatchers.is(1));
- InvalidationTracker.Observer observer = mAddedObservers.get(0);
- assertThat(consumer.mCount, CoreMatchers.is(1));
- observer.onInvalidated(tableSet);
- assertThat(consumer.mCount, CoreMatchers.is(2));
- observer.onInvalidated(tableSet);
- assertThat(consumer.mCount, CoreMatchers.is(3));
- disposable.dispose();
- observer.onInvalidated(tableSet);
- assertThat(consumer.mCount, CoreMatchers.is(3));
- }
-
- @Test
- public void internalCallable() throws InterruptedException {
- final AtomicReference<String> value = new AtomicReference<>(null);
- String[] tables = {"a", "b"};
- Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
- final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, tables,
- new Callable<String>() {
- @Override
- public String call() throws Exception {
- return value.get();
- }
- });
- final CountingConsumer consumer = new CountingConsumer();
- flowable.subscribe(consumer);
- InvalidationTracker.Observer observer = mAddedObservers.get(0);
- drain();
- // no value because it is null
- assertThat(consumer.mCount, CoreMatchers.is(0));
- value.set("bla");
- observer.onInvalidated(tableSet);
- drain();
- // get value
- assertThat(consumer.mCount, CoreMatchers.is(1));
- observer.onInvalidated(tableSet);
- drain();
- // get value
- assertThat(consumer.mCount, CoreMatchers.is(2));
- value.set(null);
- observer.onInvalidated(tableSet);
- drain();
- // no value
- assertThat(consumer.mCount, CoreMatchers.is(2));
- }
-
- private void drain() throws InterruptedException {
- mExecutor.drainTasks(2);
- }
-
- @Test
- public void exception() throws InterruptedException {
- final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, new String[]{"a"},
- new Callable<String>() {
- @Override
- public String call() throws Exception {
- throw new Exception("i want exception");
- }
- });
- TestSubscriber<String> subscriber = new TestSubscriber<>();
- flowable.subscribe(subscriber);
- drain();
- assertThat(subscriber.errorCount(), CoreMatchers.is(1));
- assertThat(subscriber.errors().get(0).getMessage(), CoreMatchers.is("i want exception"));
- }
-
- private static class CountingConsumer implements Consumer<Object> {
- int mCount = 0;
-
- @Override
- public void accept(@NonNull Object o) throws Exception {
- mCount++;
- }
- }
-}
diff --git a/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java b/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
new file mode 100644
index 0000000..15a89bf
--- /dev/null
+++ b/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 androidx.room;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.arch.core.executor.JunitTaskExecutorRule;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.reactivex.Flowable;
+import io.reactivex.annotations.NonNull;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Consumer;
+import io.reactivex.subscribers.TestSubscriber;
+
+@RunWith(JUnit4.class)
+public class RxRoomTest {
+ @Rule
+ public JunitTaskExecutorRule mExecutor = new JunitTaskExecutorRule(1, false);
+ private RoomDatabase mDatabase;
+ private InvalidationTracker mInvalidationTracker;
+ private List<InvalidationTracker.Observer> mAddedObservers = new ArrayList<>();
+
+ @Before
+ public void init() {
+ mDatabase = mock(RoomDatabase.class);
+ mInvalidationTracker = mock(InvalidationTracker.class);
+ when(mDatabase.getInvalidationTracker()).thenReturn(mInvalidationTracker);
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ mAddedObservers.add((InvalidationTracker.Observer) invocation.getArguments()[0]);
+ return null;
+ }
+ }).when(mInvalidationTracker).addObserver(any(InvalidationTracker.Observer.class));
+ }
+
+ @Test
+ public void basicAddRemove() {
+ Flowable<Object> flowable = RxRoom.createFlowable(mDatabase, "a", "b");
+ verify(mInvalidationTracker, never()).addObserver(any(InvalidationTracker.Observer.class));
+ Disposable disposable = flowable.subscribe();
+ verify(mInvalidationTracker).addObserver(any(InvalidationTracker.Observer.class));
+ assertThat(mAddedObservers.size(), CoreMatchers.is(1));
+
+ InvalidationTracker.Observer observer = mAddedObservers.get(0);
+ disposable.dispose();
+
+ verify(mInvalidationTracker).removeObserver(observer);
+
+ disposable = flowable.subscribe();
+ verify(mInvalidationTracker, times(2))
+ .addObserver(any(InvalidationTracker.Observer.class));
+ assertThat(mAddedObservers.size(), CoreMatchers.is(2));
+ assertThat(mAddedObservers.get(1), CoreMatchers.not(CoreMatchers.sameInstance(observer)));
+ InvalidationTracker.Observer observer2 = mAddedObservers.get(1);
+ disposable.dispose();
+ verify(mInvalidationTracker).removeObserver(observer2);
+ }
+
+ @Test
+ public void basicNotify() throws InterruptedException {
+ String[] tables = {"a", "b"};
+ Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
+ Flowable<Object> flowable = RxRoom.createFlowable(mDatabase, tables);
+ CountingConsumer consumer = new CountingConsumer();
+ Disposable disposable = flowable.subscribe(consumer);
+ assertThat(mAddedObservers.size(), CoreMatchers.is(1));
+ InvalidationTracker.Observer observer = mAddedObservers.get(0);
+ assertThat(consumer.mCount, CoreMatchers.is(1));
+ observer.onInvalidated(tableSet);
+ assertThat(consumer.mCount, CoreMatchers.is(2));
+ observer.onInvalidated(tableSet);
+ assertThat(consumer.mCount, CoreMatchers.is(3));
+ disposable.dispose();
+ observer.onInvalidated(tableSet);
+ assertThat(consumer.mCount, CoreMatchers.is(3));
+ }
+
+ @Test
+ public void internalCallable() throws InterruptedException {
+ final AtomicReference<String> value = new AtomicReference<>(null);
+ String[] tables = {"a", "b"};
+ Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
+ final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, tables,
+ new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ return value.get();
+ }
+ });
+ final CountingConsumer consumer = new CountingConsumer();
+ flowable.subscribe(consumer);
+ InvalidationTracker.Observer observer = mAddedObservers.get(0);
+ drain();
+ // no value because it is null
+ assertThat(consumer.mCount, CoreMatchers.is(0));
+ value.set("bla");
+ observer.onInvalidated(tableSet);
+ drain();
+ // get value
+ assertThat(consumer.mCount, CoreMatchers.is(1));
+ observer.onInvalidated(tableSet);
+ drain();
+ // get value
+ assertThat(consumer.mCount, CoreMatchers.is(2));
+ value.set(null);
+ observer.onInvalidated(tableSet);
+ drain();
+ // no value
+ assertThat(consumer.mCount, CoreMatchers.is(2));
+ }
+
+ private void drain() throws InterruptedException {
+ mExecutor.drainTasks(2);
+ }
+
+ @Test
+ public void exception() throws InterruptedException {
+ final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, new String[]{"a"},
+ new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ throw new Exception("i want exception");
+ }
+ });
+ TestSubscriber<String> subscriber = new TestSubscriber<>();
+ flowable.subscribe(subscriber);
+ drain();
+ assertThat(subscriber.errorCount(), CoreMatchers.is(1));
+ assertThat(subscriber.errors().get(0).getMessage(), CoreMatchers.is("i want exception"));
+ }
+
+ private static class CountingConsumer implements Consumer<Object> {
+ int mCount = 0;
+
+ @Override
+ public void accept(@NonNull Object o) throws Exception {
+ mCount++;
+ }
+ }
+}
diff --git a/room/testing/api/current.txt b/room/testing/api/current.txt
index e93487f..d441d62 100644
--- a/room/testing/api/current.txt
+++ b/room/testing/api/current.txt
@@ -1,12 +1,12 @@
-package android.arch.persistence.room.testing {
+package androidx.room.testing {
public class MigrationTestHelper extends org.junit.rules.TestWatcher {
ctor public MigrationTestHelper(android.app.Instrumentation, java.lang.String);
- ctor public MigrationTestHelper(android.app.Instrumentation, java.lang.String, android.arch.persistence.db.SupportSQLiteOpenHelper.Factory);
- method public void closeWhenFinished(android.arch.persistence.db.SupportSQLiteDatabase);
- method public void closeWhenFinished(android.arch.persistence.room.RoomDatabase);
- method public android.arch.persistence.db.SupportSQLiteDatabase createDatabase(java.lang.String, int) throws java.io.IOException;
- method public android.arch.persistence.db.SupportSQLiteDatabase runMigrationsAndValidate(java.lang.String, int, boolean, android.arch.persistence.room.migration.Migration...) throws java.io.IOException;
+ ctor public MigrationTestHelper(android.app.Instrumentation, java.lang.String, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory);
+ method public void closeWhenFinished(androidx.sqlite.db.SupportSQLiteDatabase);
+ method public void closeWhenFinished(androidx.room.RoomDatabase);
+ method public androidx.sqlite.db.SupportSQLiteDatabase createDatabase(java.lang.String, int) throws java.io.IOException;
+ method public androidx.sqlite.db.SupportSQLiteDatabase runMigrationsAndValidate(java.lang.String, int, boolean, androidx.room.migration.Migration...) throws java.io.IOException;
}
}
diff --git a/room/testing/api/1.0.0.txt b/room/testing/api_legacy/1.0.0.txt
similarity index 100%
rename from room/testing/api/1.0.0.txt
rename to room/testing/api_legacy/1.0.0.txt
diff --git a/room/testing/api/1.0.0.txt b/room/testing/api_legacy/1.1.0.txt
similarity index 100%
copy from room/testing/api/1.0.0.txt
copy to room/testing/api_legacy/1.1.0.txt
diff --git a/room/testing/api/1.0.0.txt b/room/testing/api_legacy/current.txt
similarity index 100%
copy from room/testing/api/1.0.0.txt
copy to room/testing/api_legacy/current.txt
diff --git a/room/testing/build.gradle b/room/testing/build.gradle
index 48ac8a7..b7162e2 100644
--- a/room/testing/build.gradle
+++ b/room/testing/build.gradle
@@ -24,12 +24,12 @@
}
dependencies {
- api(project(":room:common"))
- api(project(":room:runtime"))
- api(project(":persistence:db"))
- api(project(":persistence:db-framework"))
- api(project(":room:migration"))
- api(project(":arch:runtime"))
+ api(project(":room:room-common"))
+ api(project(":room:room-runtime"))
+ api(project(":sqlite:sqlite"))
+ api(project(":sqlite:sqlite-framework"))
+ api(project(":room:room-migration"))
+ api(project(":arch:core-runtime"))
api(SUPPORT_CORE_UTILS, libs.support_exclude_config)
api(JUNIT)
}
diff --git a/room/testing/src/main/AndroidManifest.xml b/room/testing/src/main/AndroidManifest.xml
index 594f016..16fc464 100644
--- a/room/testing/src/main/AndroidManifest.xml
+++ b/room/testing/src/main/AndroidManifest.xml
@@ -15,5 +15,5 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.arch.persistence.room.testing">
+ package="androidx.room.testing">
</manifest>
diff --git a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
deleted file mode 100644
index 3d05f8d..0000000
--- a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ /dev/null
@@ -1,438 +0,0 @@
-/*
- * 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.testing;
-
-import android.app.Instrumentation;
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
-import android.arch.persistence.room.DatabaseConfiguration;
-import android.arch.persistence.room.Room;
-import android.arch.persistence.room.RoomDatabase;
-import android.arch.persistence.room.RoomOpenHelper;
-import android.arch.persistence.room.migration.Migration;
-import android.arch.persistence.room.migration.bundle.DatabaseBundle;
-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;
-import android.database.Cursor;
-import android.util.Log;
-
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A class that can be used in your Instrumentation tests that can create the database in an
- * older schema.
- * <p>
- * You must copy the schema json files (created by passing {@code room.schemaLocation} argument
- * into the annotation processor) into your test assets and pass in the path for that folder into
- * the constructor. This class will read the folder and extract the schemas from there.
- * <pre>
- * android {
- * defaultConfig {
- * javaCompileOptions {
- * annotationProcessorOptions {
- * arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
- * }
- * }
- * }
- * sourceSets {
- * androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
- * }
- * }
- * </pre>
- */
-public class MigrationTestHelper extends TestWatcher {
- private static final String TAG = "MigrationTestHelper";
- private final String mAssetsFolder;
- private final SupportSQLiteOpenHelper.Factory mOpenFactory;
- private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>();
- private List<WeakReference<RoomDatabase>> mManagedRoomDatabases = new ArrayList<>();
- private boolean mTestStarted;
- private Instrumentation mInstrumentation;
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- *
- * @param instrumentation The instrumentation instance.
- * @param assetsFolder The asset folder in the assets directory.
- */
- public MigrationTestHelper(Instrumentation instrumentation, String assetsFolder) {
- this(instrumentation, assetsFolder, new FrameworkSQLiteOpenHelperFactory());
- }
-
- /**
- * Creates a new migration helper. It uses the Instrumentation context to load the schema
- * (falls back to the app resources) and the target context to create the database.
- *
- * @param instrumentation The instrumentation instance.
- * @param assetsFolder The asset folder in the assets directory.
- * @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper}
- */
- public MigrationTestHelper(Instrumentation instrumentation, String assetsFolder,
- SupportSQLiteOpenHelper.Factory openFactory) {
- mInstrumentation = instrumentation;
- if (assetsFolder.endsWith("/")) {
- assetsFolder = assetsFolder.substring(0, assetsFolder.length() - 1);
- }
- mAssetsFolder = assetsFolder;
- mOpenFactory = openFactory;
- }
-
- @Override
- protected void starting(Description description) {
- super.starting(description);
- mTestStarted = true;
- }
-
- /**
- * Creates the database in the given version.
- * If the database file already exists, it tries to delete it first. If delete fails, throws
- * an exception.
- *
- * @param name The name of the database.
- * @param version The version in which the database should be created.
- * @return A database connection which has the schema in the requested version.
- * @throws IOException If it cannot find the schema description in the assets folder.
- */
- @SuppressWarnings("SameParameterValue")
- public SupportSQLiteDatabase createDatabase(String name, int version) throws IOException {
- File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
- if (dbPath.exists()) {
- Log.d(TAG, "deleting database file " + name);
- if (!dbPath.delete()) {
- throw new IllegalStateException("there is a database file and i could not delete"
- + " it. Make sure you don't have any open connections to that database"
- + " before calling this method.");
- }
- }
- SchemaBundle schemaBundle = loadSchema(version);
- RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
- DatabaseConfiguration configuration = new DatabaseConfiguration(
- mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- RoomDatabase.JournalMode.TRUNCATE, true, Collections.<Integer>emptySet());
- RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
- new CreatingDelegate(schemaBundle.getDatabase()),
- schemaBundle.getDatabase().getIdentityHash(),
- // we pass the same hash twice since an old schema does not necessarily have
- // a legacy hash and we would not even persist it.
- schemaBundle.getDatabase().getIdentityHash());
- return openDatabase(name, roomOpenHelper);
- }
-
- /**
- * Runs the given set of migrations on the provided database.
- * <p>
- * It uses the same algorithm that Room uses to choose migrations so the migrations instances
- * that are provided to this method must be sufficient to bring the database from current
- * version to the desired version.
- * <p>
- * After the migration, the method validates the database schema to ensure that migration
- * result matches the expected schema. Handling of dropped tables depends on the
- * {@code validateDroppedTables} argument. If set to true, the verification will fail if it
- * finds a table that is not registered in the Database. If set to false, extra tables in the
- * database will be ignored (this is the runtime library behavior).
- *
- * @param name The database name. You must first create this database via
- * {@link #createDatabase(String, int)}.
- * @param version The final version after applying the migrations.
- * @param validateDroppedTables If set to true, validation will fail if the database has
- * unknown
- * tables.
- * @param migrations The list of available migrations.
- * @throws IOException If it cannot find the schema for {@code toVersion}.
- * @throws IllegalStateException If the schema validation fails.
- */
- public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
- boolean validateDroppedTables, Migration... migrations) throws IOException {
- File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
- if (!dbPath.exists()) {
- throw new IllegalStateException("Cannot find the database file for " + name + ". "
- + "Before calling runMigrations, you must first create the database via "
- + "createDatabase.");
- }
- SchemaBundle schemaBundle = loadSchema(version);
- RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
- container.addMigrations(migrations);
- DatabaseConfiguration configuration = new DatabaseConfiguration(
- mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- RoomDatabase.JournalMode.TRUNCATE, true, Collections.<Integer>emptySet());
- RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
- new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
- // we pass the same hash twice since an old schema does not necessarily have
- // a legacy hash and we would not even persist it.
- schemaBundle.getDatabase().getIdentityHash(),
- schemaBundle.getDatabase().getIdentityHash());
- return openDatabase(name, roomOpenHelper);
- }
-
- private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) {
- SupportSQLiteOpenHelper.Configuration config =
- SupportSQLiteOpenHelper.Configuration
- .builder(mInstrumentation.getTargetContext())
- .callback(roomOpenHelper)
- .name(name)
- .build();
- SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
- mManagedDatabases.add(new WeakReference<>(db));
- return db;
- }
-
- @Override
- protected void finished(Description description) {
- super.finished(description);
- for (WeakReference<SupportSQLiteDatabase> dbRef : mManagedDatabases) {
- SupportSQLiteDatabase db = dbRef.get();
- if (db != null && db.isOpen()) {
- try {
- db.close();
- } catch (Throwable ignored) {
- }
- }
- }
- for (WeakReference<RoomDatabase> dbRef : mManagedRoomDatabases) {
- final RoomDatabase roomDatabase = dbRef.get();
- if (roomDatabase != null) {
- roomDatabase.close();
- }
- }
- }
-
- /**
- * Registers a database connection to be automatically closed when the test finishes.
- * <p>
- * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
- * {@link org.junit.Rule Rule} annotation.
- *
- * @param db The database connection that should be closed after the test finishes.
- */
- public void closeWhenFinished(SupportSQLiteDatabase db) {
- if (!mTestStarted) {
- throw new IllegalStateException("You cannot register a database to be closed before"
- + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
- + " test rule? (@Rule)");
- }
- mManagedDatabases.add(new WeakReference<>(db));
- }
-
- /**
- * Registers a database connection to be automatically closed when the test finishes.
- * <p>
- * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
- * {@link org.junit.Rule Rule} annotation.
- *
- * @param db The RoomDatabase instance which holds the database.
- */
- public void closeWhenFinished(RoomDatabase db) {
- if (!mTestStarted) {
- throw new IllegalStateException("You cannot register a database to be closed before"
- + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
- + " test rule? (@Rule)");
- }
- mManagedRoomDatabases.add(new WeakReference<>(db));
- }
-
- private SchemaBundle loadSchema(int version) throws IOException {
- try {
- return loadSchema(mInstrumentation.getContext(), version);
- } catch (FileNotFoundException testAssetsIOExceptions) {
- Log.w(TAG, "Could not find the schema file in the test assets. Checking the"
- + " application assets");
- try {
- return loadSchema(mInstrumentation.getTargetContext(), version);
- } catch (FileNotFoundException appAssetsException) {
- // throw the test assets exception instead
- throw new FileNotFoundException("Cannot find the schema file in the assets folder. "
- + "Make sure to include the exported json schemas in your test assert "
- + "inputs. See "
- + "https://developer.android.com/topic/libraries/architecture/"
- + "room.html#db-migration-testing for details. Missing file: "
- + testAssetsIOExceptions.getMessage());
- }
- }
- }
-
- private SchemaBundle loadSchema(Context context, int version) throws IOException {
- InputStream input = context.getAssets().open(mAssetsFolder + "/" + version + ".json");
- return SchemaBundle.deserialize(input);
- }
-
- private static TableInfo toTableInfo(EntityBundle entityBundle) {
- return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle),
- 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(
- List<ForeignKeyBundle> bundles) {
- if (bundles == null) {
- return Collections.emptySet();
- }
- Set<TableInfo.ForeignKey> result = new HashSet<>(bundles.size());
- for (ForeignKeyBundle bundle : bundles) {
- result.add(new TableInfo.ForeignKey(bundle.getTable(),
- bundle.getOnDelete(), bundle.getOnUpdate(),
- bundle.getColumns(), bundle.getReferencedColumns()));
- }
- return result;
- }
-
- private static Map<String, TableInfo.Column> toColumnMap(EntityBundle entity) {
- Map<String, TableInfo.Column> result = new HashMap<>();
- for (FieldBundle bundle : entity.getFields()) {
- TableInfo.Column column = toColumn(entity, bundle);
- result.put(column.name, column);
- }
- return result;
- }
-
- private static TableInfo.Column toColumn(EntityBundle entity, FieldBundle field) {
- return new TableInfo.Column(field.getColumnName(), field.getAffinity(),
- field.isNonNull(), findPrimaryKeyPosition(entity, field));
- }
-
- private static int findPrimaryKeyPosition(EntityBundle entity, FieldBundle field) {
- List<String> columnNames = entity.getPrimaryKey().getColumnNames();
- int i = 0;
- for (String columnName : columnNames) {
- i++;
- if (field.getColumnName().equalsIgnoreCase(columnName)) {
- return i;
- }
- }
- return 0;
- }
-
- static class MigratingDelegate extends RoomOpenHelperDelegate {
- private final boolean mVerifyDroppedTables;
-
- MigratingDelegate(DatabaseBundle databaseBundle, boolean verifyDroppedTables) {
- super(databaseBundle);
- mVerifyDroppedTables = verifyDroppedTables;
- }
-
- @Override
- protected void createAllTables(SupportSQLiteDatabase database) {
- throw new UnsupportedOperationException("Was expecting to migrate but received create."
- + "Make sure you have created the database first.");
- }
-
- @Override
- protected void validateMigration(SupportSQLiteDatabase db) {
- final Map<String, EntityBundle> tables = mDatabaseBundle.getEntitiesByTableName();
- for (EntityBundle entity : tables.values()) {
- final TableInfo expected = toTableInfo(entity);
- final TableInfo found = TableInfo.read(db, entity.getTableName());
- if (!expected.equals(found)) {
- throw new IllegalStateException(
- "Migration failed. expected:" + expected + " , found:" + found);
- }
- }
- if (mVerifyDroppedTables) {
- // now ensure tables that should be removed are removed.
- Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type='table'"
- + " AND name NOT IN(?, ?, ?)",
- new String[]{Room.MASTER_TABLE_NAME, "android_metadata",
- "sqlite_sequence"});
- //noinspection TryFinallyCanBeTryWithResources
- try {
- while (cursor.moveToNext()) {
- final String tableName = cursor.getString(0);
- if (!tables.containsKey(tableName)) {
- throw new IllegalStateException("Migration failed. Unexpected table "
- + tableName);
- }
- }
- } finally {
- cursor.close();
- }
- }
- }
- }
-
- static class CreatingDelegate extends RoomOpenHelperDelegate {
-
- CreatingDelegate(DatabaseBundle databaseBundle) {
- super(databaseBundle);
- }
-
- @Override
- protected void createAllTables(SupportSQLiteDatabase database) {
- for (String query : mDatabaseBundle.buildCreateQueries()) {
- database.execSQL(query);
- }
- }
-
- @Override
- protected void validateMigration(SupportSQLiteDatabase db) {
- throw new UnsupportedOperationException("This open helper just creates the database but"
- + " it received a migration request.");
- }
- }
-
- abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate {
- final DatabaseBundle mDatabaseBundle;
-
- RoomOpenHelperDelegate(DatabaseBundle databaseBundle) {
- super(databaseBundle.getVersion());
- mDatabaseBundle = databaseBundle;
- }
-
- @Override
- protected void dropAllTables(SupportSQLiteDatabase database) {
- throw new UnsupportedOperationException("cannot drop all tables in the test");
- }
-
- @Override
- protected void onCreate(SupportSQLiteDatabase database) {
- }
-
- @Override
- protected void onOpen(SupportSQLiteDatabase database) {
- }
- }
-}
diff --git a/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
new file mode 100644
index 0000000..3708fff
--- /dev/null
+++ b/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
@@ -0,0 +1,439 @@
+/*
+ * 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 androidx.room.testing;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.database.Cursor;
+import android.util.Log;
+
+import androidx.room.DatabaseConfiguration;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.RoomOpenHelper;
+import androidx.room.migration.Migration;
+import androidx.room.migration.bundle.DatabaseBundle;
+import androidx.room.migration.bundle.EntityBundle;
+import androidx.room.migration.bundle.FieldBundle;
+import androidx.room.migration.bundle.ForeignKeyBundle;
+import androidx.room.migration.bundle.IndexBundle;
+import androidx.room.migration.bundle.SchemaBundle;
+import androidx.room.util.TableInfo;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class that can be used in your Instrumentation tests that can create the database in an
+ * older schema.
+ * <p>
+ * You must copy the schema json files (created by passing {@code room.schemaLocation} argument
+ * into the annotation processor) into your test assets and pass in the path for that folder into
+ * the constructor. This class will read the folder and extract the schemas from there.
+ * <pre>
+ * android {
+ * defaultConfig {
+ * javaCompileOptions {
+ * annotationProcessorOptions {
+ * arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
+ * }
+ * }
+ * }
+ * sourceSets {
+ * androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ * }
+ * }
+ * </pre>
+ */
+public class MigrationTestHelper extends TestWatcher {
+ private static final String TAG = "MigrationTestHelper";
+ private final String mAssetsFolder;
+ private final SupportSQLiteOpenHelper.Factory mOpenFactory;
+ private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>();
+ private List<WeakReference<RoomDatabase>> mManagedRoomDatabases = new ArrayList<>();
+ private boolean mTestStarted;
+ private Instrumentation mInstrumentation;
+
+ /**
+ * Creates a new migration helper. It uses the Instrumentation context to load the schema
+ * (falls back to the app resources) and the target context to create the database.
+ *
+ * @param instrumentation The instrumentation instance.
+ * @param assetsFolder The asset folder in the assets directory.
+ */
+ public MigrationTestHelper(Instrumentation instrumentation, String assetsFolder) {
+ this(instrumentation, assetsFolder, new FrameworkSQLiteOpenHelperFactory());
+ }
+
+ /**
+ * Creates a new migration helper. It uses the Instrumentation context to load the schema
+ * (falls back to the app resources) and the target context to create the database.
+ *
+ * @param instrumentation The instrumentation instance.
+ * @param assetsFolder The asset folder in the assets directory.
+ * @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper}
+ */
+ public MigrationTestHelper(Instrumentation instrumentation, String assetsFolder,
+ SupportSQLiteOpenHelper.Factory openFactory) {
+ mInstrumentation = instrumentation;
+ if (assetsFolder.endsWith("/")) {
+ assetsFolder = assetsFolder.substring(0, assetsFolder.length() - 1);
+ }
+ mAssetsFolder = assetsFolder;
+ mOpenFactory = openFactory;
+ }
+
+ @Override
+ protected void starting(Description description) {
+ super.starting(description);
+ mTestStarted = true;
+ }
+
+ /**
+ * Creates the database in the given version.
+ * If the database file already exists, it tries to delete it first. If delete fails, throws
+ * an exception.
+ *
+ * @param name The name of the database.
+ * @param version The version in which the database should be created.
+ * @return A database connection which has the schema in the requested version.
+ * @throws IOException If it cannot find the schema description in the assets folder.
+ */
+ @SuppressWarnings("SameParameterValue")
+ public SupportSQLiteDatabase createDatabase(String name, int version) throws IOException {
+ File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
+ if (dbPath.exists()) {
+ Log.d(TAG, "deleting database file " + name);
+ if (!dbPath.delete()) {
+ throw new IllegalStateException("there is a database file and i could not delete"
+ + " it. Make sure you don't have any open connections to that database"
+ + " before calling this method.");
+ }
+ }
+ SchemaBundle schemaBundle = loadSchema(version);
+ RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
+ DatabaseConfiguration configuration = new DatabaseConfiguration(
+ mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
+ RoomDatabase.JournalMode.TRUNCATE, true, Collections.<Integer>emptySet());
+ RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
+ new CreatingDelegate(schemaBundle.getDatabase()),
+ schemaBundle.getDatabase().getIdentityHash(),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ schemaBundle.getDatabase().getIdentityHash());
+ return openDatabase(name, roomOpenHelper);
+ }
+
+ /**
+ * Runs the given set of migrations on the provided database.
+ * <p>
+ * It uses the same algorithm that Room uses to choose migrations so the migrations instances
+ * that are provided to this method must be sufficient to bring the database from current
+ * version to the desired version.
+ * <p>
+ * After the migration, the method validates the database schema to ensure that migration
+ * result matches the expected schema. Handling of dropped tables depends on the
+ * {@code validateDroppedTables} argument. If set to true, the verification will fail if it
+ * finds a table that is not registered in the Database. If set to false, extra tables in the
+ * database will be ignored (this is the runtime library behavior).
+ *
+ * @param name The database name. You must first create this database via
+ * {@link #createDatabase(String, int)}.
+ * @param version The final version after applying the migrations.
+ * @param validateDroppedTables If set to true, validation will fail if the database has
+ * unknown
+ * tables.
+ * @param migrations The list of available migrations.
+ * @throws IOException If it cannot find the schema for {@code toVersion}.
+ * @throws IllegalStateException If the schema validation fails.
+ */
+ public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
+ boolean validateDroppedTables, Migration... migrations) throws IOException {
+ File dbPath = mInstrumentation.getTargetContext().getDatabasePath(name);
+ if (!dbPath.exists()) {
+ throw new IllegalStateException("Cannot find the database file for " + name + ". "
+ + "Before calling runMigrations, you must first create the database via "
+ + "createDatabase.");
+ }
+ SchemaBundle schemaBundle = loadSchema(version);
+ RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
+ container.addMigrations(migrations);
+ DatabaseConfiguration configuration = new DatabaseConfiguration(
+ mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
+ RoomDatabase.JournalMode.TRUNCATE, true, Collections.<Integer>emptySet());
+ RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
+ new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ schemaBundle.getDatabase().getIdentityHash(),
+ schemaBundle.getDatabase().getIdentityHash());
+ return openDatabase(name, roomOpenHelper);
+ }
+
+ private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) {
+ SupportSQLiteOpenHelper.Configuration config =
+ SupportSQLiteOpenHelper.Configuration
+ .builder(mInstrumentation.getTargetContext())
+ .callback(roomOpenHelper)
+ .name(name)
+ .build();
+ SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase();
+ mManagedDatabases.add(new WeakReference<>(db));
+ return db;
+ }
+
+ @Override
+ protected void finished(Description description) {
+ super.finished(description);
+ for (WeakReference<SupportSQLiteDatabase> dbRef : mManagedDatabases) {
+ SupportSQLiteDatabase db = dbRef.get();
+ if (db != null && db.isOpen()) {
+ try {
+ db.close();
+ } catch (Throwable ignored) {
+ }
+ }
+ }
+ for (WeakReference<RoomDatabase> dbRef : mManagedRoomDatabases) {
+ final RoomDatabase roomDatabase = dbRef.get();
+ if (roomDatabase != null) {
+ roomDatabase.close();
+ }
+ }
+ }
+
+ /**
+ * Registers a database connection to be automatically closed when the test finishes.
+ * <p>
+ * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
+ * {@link org.junit.Rule Rule} annotation.
+ *
+ * @param db The database connection that should be closed after the test finishes.
+ */
+ public void closeWhenFinished(SupportSQLiteDatabase db) {
+ if (!mTestStarted) {
+ throw new IllegalStateException("You cannot register a database to be closed before"
+ + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
+ + " test rule? (@Rule)");
+ }
+ mManagedDatabases.add(new WeakReference<>(db));
+ }
+
+ /**
+ * Registers a database connection to be automatically closed when the test finishes.
+ * <p>
+ * This only works if {@code MigrationTestHelper} is registered as a Junit test rule via
+ * {@link org.junit.Rule Rule} annotation.
+ *
+ * @param db The RoomDatabase instance which holds the database.
+ */
+ public void closeWhenFinished(RoomDatabase db) {
+ if (!mTestStarted) {
+ throw new IllegalStateException("You cannot register a database to be closed before"
+ + " the test starts. Maybe you forgot to annotate MigrationTestHelper as a"
+ + " test rule? (@Rule)");
+ }
+ mManagedRoomDatabases.add(new WeakReference<>(db));
+ }
+
+ private SchemaBundle loadSchema(int version) throws IOException {
+ try {
+ return loadSchema(mInstrumentation.getContext(), version);
+ } catch (FileNotFoundException testAssetsIOExceptions) {
+ Log.w(TAG, "Could not find the schema file in the test assets. Checking the"
+ + " application assets");
+ try {
+ return loadSchema(mInstrumentation.getTargetContext(), version);
+ } catch (FileNotFoundException appAssetsException) {
+ // throw the test assets exception instead
+ throw new FileNotFoundException("Cannot find the schema file in the assets folder. "
+ + "Make sure to include the exported json schemas in your test assert "
+ + "inputs. See "
+ + "https://developer.android.com/topic/libraries/architecture/"
+ + "room.html#db-migration-testing for details. Missing file: "
+ + testAssetsIOExceptions.getMessage());
+ }
+ }
+ }
+
+ private SchemaBundle loadSchema(Context context, int version) throws IOException {
+ InputStream input = context.getAssets().open(mAssetsFolder + "/" + version + ".json");
+ return SchemaBundle.deserialize(input);
+ }
+
+ private static TableInfo toTableInfo(EntityBundle entityBundle) {
+ return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle),
+ 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(
+ List<ForeignKeyBundle> bundles) {
+ if (bundles == null) {
+ return Collections.emptySet();
+ }
+ Set<TableInfo.ForeignKey> result = new HashSet<>(bundles.size());
+ for (ForeignKeyBundle bundle : bundles) {
+ result.add(new TableInfo.ForeignKey(bundle.getTable(),
+ bundle.getOnDelete(), bundle.getOnUpdate(),
+ bundle.getColumns(), bundle.getReferencedColumns()));
+ }
+ return result;
+ }
+
+ private static Map<String, TableInfo.Column> toColumnMap(EntityBundle entity) {
+ Map<String, TableInfo.Column> result = new HashMap<>();
+ for (FieldBundle bundle : entity.getFields()) {
+ TableInfo.Column column = toColumn(entity, bundle);
+ result.put(column.name, column);
+ }
+ return result;
+ }
+
+ private static TableInfo.Column toColumn(EntityBundle entity, FieldBundle field) {
+ return new TableInfo.Column(field.getColumnName(), field.getAffinity(),
+ field.isNonNull(), findPrimaryKeyPosition(entity, field));
+ }
+
+ private static int findPrimaryKeyPosition(EntityBundle entity, FieldBundle field) {
+ List<String> columnNames = entity.getPrimaryKey().getColumnNames();
+ int i = 0;
+ for (String columnName : columnNames) {
+ i++;
+ if (field.getColumnName().equalsIgnoreCase(columnName)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ static class MigratingDelegate extends RoomOpenHelperDelegate {
+ private final boolean mVerifyDroppedTables;
+
+ MigratingDelegate(DatabaseBundle databaseBundle, boolean verifyDroppedTables) {
+ super(databaseBundle);
+ mVerifyDroppedTables = verifyDroppedTables;
+ }
+
+ @Override
+ protected void createAllTables(SupportSQLiteDatabase database) {
+ throw new UnsupportedOperationException("Was expecting to migrate but received create."
+ + "Make sure you have created the database first.");
+ }
+
+ @Override
+ protected void validateMigration(SupportSQLiteDatabase db) {
+ final Map<String, EntityBundle> tables = mDatabaseBundle.getEntitiesByTableName();
+ for (EntityBundle entity : tables.values()) {
+ final TableInfo expected = toTableInfo(entity);
+ final TableInfo found = TableInfo.read(db, entity.getTableName());
+ if (!expected.equals(found)) {
+ throw new IllegalStateException(
+ "Migration failed. expected:" + expected + " , found:" + found);
+ }
+ }
+ if (mVerifyDroppedTables) {
+ // now ensure tables that should be removed are removed.
+ Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type='table'"
+ + " AND name NOT IN(?, ?, ?)",
+ new String[]{Room.MASTER_TABLE_NAME, "android_metadata",
+ "sqlite_sequence"});
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ while (cursor.moveToNext()) {
+ final String tableName = cursor.getString(0);
+ if (!tables.containsKey(tableName)) {
+ throw new IllegalStateException("Migration failed. Unexpected table "
+ + tableName);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+ }
+
+ static class CreatingDelegate extends RoomOpenHelperDelegate {
+
+ CreatingDelegate(DatabaseBundle databaseBundle) {
+ super(databaseBundle);
+ }
+
+ @Override
+ protected void createAllTables(SupportSQLiteDatabase database) {
+ for (String query : mDatabaseBundle.buildCreateQueries()) {
+ database.execSQL(query);
+ }
+ }
+
+ @Override
+ protected void validateMigration(SupportSQLiteDatabase db) {
+ throw new UnsupportedOperationException("This open helper just creates the database but"
+ + " it received a migration request.");
+ }
+ }
+
+ abstract static class RoomOpenHelperDelegate extends RoomOpenHelper.Delegate {
+ final DatabaseBundle mDatabaseBundle;
+
+ RoomOpenHelperDelegate(DatabaseBundle databaseBundle) {
+ super(databaseBundle.getVersion());
+ mDatabaseBundle = databaseBundle;
+ }
+
+ @Override
+ protected void dropAllTables(SupportSQLiteDatabase database) {
+ throw new UnsupportedOperationException("cannot drop all tables in the test");
+ }
+
+ @Override
+ protected void onCreate(SupportSQLiteDatabase database) {
+ }
+
+ @Override
+ protected void onOpen(SupportSQLiteDatabase database) {
+ }
+ }
+}
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/BrowseFragment.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/BrowseFragment.java
index aa53d49..16bd6be 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/BrowseFragment.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/BrowseFragment.java
@@ -19,6 +19,8 @@
import android.content.Context;
import android.os.Bundle;
import android.os.RemoteException;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -33,8 +35,6 @@
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.session.MediaControllerCompat;
import com.example.android.supportv4.R;
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserServiceSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserServiceSupport.java
index 0e6701e..dc0cd81 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserServiceSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserServiceSupport.java
@@ -29,6 +29,8 @@
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
@@ -36,8 +38,6 @@
import android.text.TextUtils;
import android.util.Log;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.MediaBrowserCompat.MediaItem;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.session.MediaButtonReceiver;
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserSupport.java
index bf8e27c..8dd2408 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaBrowserSupport.java
@@ -17,10 +17,10 @@
package com.example.android.supportv4.media;
import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import androidx.fragment.app.FragmentActivity;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.session.MediaControllerCompat;
import com.example.android.supportv4.R;
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaNotificationManager.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaNotificationManager.java
index 5a118e6..c6bf935 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaNotificationManager.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/MediaNotificationManager.java
@@ -28,13 +28,13 @@
import android.os.RemoteException;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
-import androidx.media.session.MediaControllerCompat;
import com.example.android.supportv4.R;
import com.example.android.supportv4.media.utils.ResourceHelper;
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/QueueFragment.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/QueueFragment.java
index 55e54c8..7ca8c9c 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/QueueFragment.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/media/QueueFragment.java
@@ -19,6 +19,8 @@
import android.content.ComponentName;
import android.os.Bundle;
import android.os.RemoteException;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
@@ -31,8 +33,6 @@
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
-import androidx.media.MediaBrowserCompat;
-import androidx.media.session.MediaControllerCompat;
import com.example.android.supportv4.R;
diff --git a/samples/SupportDesignDemos/build.gradle b/samples/SupportDesignDemos/build.gradle
index b554991..8721e2b 100644
--- a/samples/SupportDesignDemos/build.gradle
+++ b/samples/SupportDesignDemos/build.gradle
@@ -1,6 +1,5 @@
plugins {
id("SupportAndroidTestAppPlugin")
- id("androidx.tools.jetifier")
}
dependencies {
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/MusicPlayerFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/MusicPlayerFragment.java
index 6106355..1fd7af1 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/MusicPlayerFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/MusicPlayerFragment.java
@@ -22,6 +22,7 @@
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
+import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.leanback.app.PlaybackFragment;
@@ -32,7 +33,6 @@
import androidx.leanback.widget.Action;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.PlaybackControlsRow;
-import androidx.media.session.MediaControllerCompat;
import com.google.gson.Gson;
diff --git a/samples/SupportLeanbackJank/build.gradle b/samples/SupportLeanbackJank/build.gradle
index 43ed95d..199e545 100644
--- a/samples/SupportLeanbackJank/build.gradle
+++ b/samples/SupportLeanbackJank/build.gradle
@@ -1,10 +1,9 @@
plugins {
id("SupportAndroidTestAppPlugin")
- id("androidx.tools.jetifier")
}
dependencies {
- implementation(jetifier.process('com.github.bumptech.glide:glide:3.6.1'))
+ implementation('com.github.bumptech.glide:glide:3.6.1')
implementation(project(":leanback"))
implementation(project(":leanback-preference"))
}
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
index dfb82f5..379b991 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
@@ -39,7 +39,7 @@
import androidx.annotation.NonNull;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
-import androidx.slice.builders.GridBuilder;
+import androidx.slice.builders.GridRowBuilder;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.MessagingSliceBuilder;
import androidx.slice.builders.SliceAction;
@@ -214,10 +214,10 @@
private Slice createCatSlice(Uri sliceUri, boolean customSeeMore) {
ListBuilder b = new ListBuilder(getContext(), sliceUri);
- GridBuilder gb = new GridBuilder(b);
+ GridRowBuilder gb = new GridRowBuilder(b);
PendingIntent pi = getBroadcastIntent(ACTION_TOAST, "See cats you follow");
if (customSeeMore) {
- GridBuilder.CellBuilder cb = new GridBuilder.CellBuilder(gb);
+ GridRowBuilder.CellBuilder cb = new GridRowBuilder.CellBuilder(gb);
cb.addImage(Icon.createWithResource(getContext(), R.drawable.ic_right_caret),
ICON_IMAGE);
cb.setContentIntent(pi);
@@ -226,59 +226,59 @@
} else {
gb.addSeeMoreAction(pi);
}
- gb.addCell(new GridBuilder.CellBuilder(gb)
+ gb.addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_1), SMALL_IMAGE)
.addTitleText("Oreo"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_2),
SMALL_IMAGE)
.addTitleText("Silver"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_3),
SMALL_IMAGE)
.addTitleText("Drake"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_5),
SMALL_IMAGE)
.addTitleText("Olive"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_4),
SMALL_IMAGE)
.addTitleText("Lady Marmalade"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_6),
SMALL_IMAGE)
.addTitleText("Grapefruit"));
- return b.addGrid(gb).build();
+ return b.addGridRow(gb).build();
}
private Slice createContact2(Uri sliceUri) {
ListBuilder b = new ListBuilder(getContext(), sliceUri);
ListBuilder.RowBuilder rb = new ListBuilder.RowBuilder(b);
- GridBuilder gb = new GridBuilder(b);
+ GridRowBuilder gb = new GridRowBuilder(b);
return b.setColor(0xff3949ab)
.addRow(rb
.setTitle("Mady Pitza")
.setSubtitle("Frequently contacted contact")
.addEndItem(Icon.createWithResource(getContext(), R.drawable.mady),
SMALL_IMAGE))
- .addGrid(gb
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addGridRow(gb
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.ic_call),
ICON_IMAGE)
.addText("Call")
.setContentIntent(getBroadcastIntent(ACTION_TOAST, "call")))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.ic_text),
ICON_IMAGE)
.addText("Text")
.setContentIntent(getBroadcastIntent(ACTION_TOAST, "text")))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(),
R.drawable.ic_video), ICON_IMAGE)
.setContentIntent(getBroadcastIntent(ACTION_TOAST, "video"))
.addText("Video"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(),
R.drawable.ic_email), ICON_IMAGE)
.addText("Email")
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
index a5b77e3..174705e 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
@@ -19,7 +19,6 @@
import static com.example.androidx.slice.demos.SampleSliceProvider.URI_PATHS;
import static com.example.androidx.slice.demos.SampleSliceProvider.getUri;
-import android.arch.lifecycle.LiveData;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -45,6 +44,7 @@
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.LiveData;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.widget.EventInfo;
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.java
index 666f7d2..bdf0b49 100644
--- a/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.java
+++ b/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/CardFragmentActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 The Android Open Source Project
+ * Copyright (C) 2018 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.
diff --git a/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/cards/CardView.java b/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/cards/CardView.java
index 02b52f8..563f73d 100644
--- a/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/cards/CardView.java
+++ b/samples/ViewPager2Demos/src/main/java/com/example/androidx/viewpager2/cards/CardView.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 The Android Open Source Project
+ * Copyright (C) 2018 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.
diff --git a/settings.gradle b/settings.gradle
index c402bf3..dde4d2f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -33,19 +33,26 @@
//
/////////////////////////////
-includeProject(":vectordrawable-animated", "graphics/drawable/animated")
+includeProject(":annotation", "annotations")
includeProject(":appcompat", "v7/appcompat")
includeProject(":asynclayoutinflater", "asynclayoutinflater")
+includeProject(":browser", "browser")
includeProject(":car", "car")
includeProject(":cardview", "cardview")
includeProject(":collection", "collection")
+includeProject(":contentpaging", "content")
includeProject(":coordinatorlayout", "coordinatorlayout")
+includeProject(":core", "compat")
includeProject(":cursoradapter", "cursoradapter")
-includeProject(":browser", "browser")
includeProject(":customview", "customview")
includeProject(":documentfile", "documentfile")
includeProject(":drawerlayout", "drawerlayout")
+includeProject(":dynamicanimation", "dynamic-animation")
+includeProject(":emoji", "emoji/core")
+includeProject(":emoji-bundled", "emoji/bundled")
+includeProject(":emoji-appcompat", "emoji/appcompat")
includeProject(":exifinterface", "exifinterface")
+includeProject(":fragment", "fragment")
includeProject(":gridlayout", "gridlayout")
includeProject(":heifwriter", "heifwriter")
includeProject(":interpolator", "interpolator")
@@ -54,13 +61,14 @@
includeProject(":jetifier-standalone", "jetifier/jetifier/standalone")
includeProject(":jetifier-preprocessor", "jetifier/jetifier/preprocessor")
includeProject(":leanback", "leanback")
+includeProject(":leanback-preference", "leanback-preference")
includeProject(":loader", "loader")
includeProject(":localbroadcastmanager", "localbroadcastmanager")
+includeProject(":media", "media")
includeProject(":mediarouter", "mediarouter")
includeProject(":palette", "palette")
includeProject(":percentlayout", "percent")
includeProject(":preference", "preference")
-includeProject(":leanback-preference", "leanback-preference")
includeProject(":print", "print")
includeProject(":recommendation", "recommendation")
includeProject(":recyclerview", "v7/recyclerview")
@@ -69,20 +77,12 @@
includeProject(":slices-view", "slices/view")
includeProject(":slices-builders", "slices/builders")
includeProject(":slidingpanelayout", "slidingpanelayout")
-includeProject(":annotation", "annotations")
-includeProject(":core", "compat")
-includeProject(":contentpaging", "content")
-includeProject(":dynamicanimation", "dynamic-animation")
-includeProject(":emoji", "emoji/core")
-includeProject(":emoji-bundled", "emoji/bundled")
-includeProject(":emoji-appcompat", "emoji/appcompat")
-includeProject(":fragment", "fragment")
-includeProject(":media", "media")
-includeProject(":tvprovider", "tv-provider")
-includeProject(":vectordrawable", "graphics/drawable/static")
includeProject(":swiperefreshlayout", "swiperefreshlayout")
includeProject(":textclassifier", "textclassifier")
includeProject(":transition", "transition")
+includeProject(":tvprovider", "tv-provider")
+includeProject(":vectordrawable", "graphics/drawable/static")
+includeProject(":vectordrawable-animated", "graphics/drawable/animated")
includeProject(":viewpager", "viewpager")
includeProject(":viewpager2", "viewpager2")
includeProject(":wear", "wear")
@@ -116,7 +116,7 @@
includeProject(":support-design-demos", new File(samplesRoot, "SupportDesignDemos"))
includeProject(":support-emoji-demos", new File(samplesRoot, "SupportEmojiDemos"))
includeProject(":support-leanback-demos", new File(samplesRoot, "SupportLeanbackDemos"))
-includeProject(":support-leanback-jank", new File(samplesRoot, "SupportLeanbackJank"))
+//includeProject(":support-leanback-jank", new File(samplesRoot, "SupportLeanbackJank"))
includeProject(":support-percent-demos", new File(samplesRoot, "SupportPercentDemos"))
includeProject(":support-preference-demos", new File(samplesRoot, "SupportPreferenceDemos"))
includeProject(":support-slices-demos", new File(samplesRoot, "SupportSliceDemos"))
diff --git a/slices/builders/api/current.txt b/slices/builders/api/current.txt
index 96b8d75..ed3d684 100644
--- a/slices/builders/api/current.txt
+++ b/slices/builders/api/current.txt
@@ -1,6 +1,6 @@
package androidx.slice.builders {
- public class GridBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+ public deprecated class GridBuilder extends androidx.slice.builders.TemplateSliceBuilder {
ctor public GridBuilder(androidx.slice.builders.ListBuilder);
method public androidx.slice.builders.GridBuilder addCell(androidx.slice.builders.GridBuilder.CellBuilder);
method public androidx.slice.builders.GridBuilder addCell(java.util.function.Consumer<androidx.slice.builders.GridBuilder.CellBuilder>);
@@ -14,7 +14,7 @@
field public static final deprecated int SMALL_IMAGE = 1; // 0x1
}
- public static final class GridBuilder.CellBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+ public static final deprecated class GridBuilder.CellBuilder extends androidx.slice.builders.TemplateSliceBuilder {
ctor public GridBuilder.CellBuilder(androidx.slice.builders.GridBuilder);
ctor public GridBuilder.CellBuilder(androidx.slice.builders.GridBuilder, android.net.Uri);
method public deprecated androidx.slice.builders.GridBuilder.CellBuilder addImage(android.graphics.drawable.Icon);
@@ -31,11 +31,37 @@
method public androidx.slice.builders.GridBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
}
+ public class GridRowBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+ ctor public GridRowBuilder(androidx.slice.builders.ListBuilder);
+ method public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+ method public androidx.slice.builders.GridRowBuilder addCell(java.util.function.Consumer<androidx.slice.builders.GridRowBuilder.CellBuilder>);
+ method public androidx.slice.builders.GridRowBuilder addSeeMoreAction(android.app.PendingIntent);
+ method public androidx.slice.builders.GridRowBuilder addSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+ method public androidx.slice.builders.GridRowBuilder addSeeMoreCell(java.util.function.Consumer<androidx.slice.builders.GridRowBuilder.CellBuilder>);
+ method public androidx.slice.builders.GridRowBuilder setContentDescription(java.lang.CharSequence);
+ method public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+ }
+
+ public static final class GridRowBuilder.CellBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+ ctor public GridRowBuilder.CellBuilder(androidx.slice.builders.GridRowBuilder);
+ ctor public GridRowBuilder.CellBuilder(androidx.slice.builders.GridRowBuilder, android.net.Uri);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(android.graphics.drawable.Icon, int);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(android.graphics.drawable.Icon, int, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(java.lang.CharSequence);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(java.lang.CharSequence, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(java.lang.CharSequence);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(java.lang.CharSequence, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(java.lang.CharSequence);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
+ }
+
public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
ctor public ListBuilder(android.content.Context, android.net.Uri);
method public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
- method public androidx.slice.builders.ListBuilder addGrid(androidx.slice.builders.GridBuilder);
- method public androidx.slice.builders.ListBuilder addGrid(java.util.function.Consumer<androidx.slice.builders.GridBuilder>);
+ method public deprecated androidx.slice.builders.ListBuilder addGrid(androidx.slice.builders.GridBuilder);
+ method public deprecated androidx.slice.builders.ListBuilder addGrid(java.util.function.Consumer<androidx.slice.builders.GridBuilder>);
+ method public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
+ method public androidx.slice.builders.ListBuilder addGridRow(java.util.function.Consumer<androidx.slice.builders.GridRowBuilder>);
method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
method public androidx.slice.builders.ListBuilder addInputRange(java.util.function.Consumer<androidx.slice.builders.ListBuilder.InputRangeBuilder>);
method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
diff --git a/slices/builders/api_legacy/current.txt b/slices/builders/api_legacy/current.txt
index 5aa4f42..8b00abf 100644
--- a/slices/builders/api_legacy/current.txt
+++ b/slices/builders/api_legacy/current.txt
@@ -2,38 +2,38 @@
public class GridBuilder extends androidx.slice.builders.TemplateSliceBuilder {
ctor public GridBuilder(androidx.slice.builders.ListBuilder);
- method public androidx.slice.builders.GridBuilder addCell(androidx.slice.builders.GridBuilder.CellBuilder);
- method public androidx.slice.builders.GridBuilder addCell(java.util.function.Consumer<androidx.slice.builders.GridBuilder.CellBuilder>);
- method public androidx.slice.builders.GridBuilder addSeeMoreAction(android.app.PendingIntent);
- method public androidx.slice.builders.GridBuilder addSeeMoreCell(androidx.slice.builders.GridBuilder.CellBuilder);
- method public androidx.slice.builders.GridBuilder addSeeMoreCell(java.util.function.Consumer<androidx.slice.builders.GridBuilder.CellBuilder>);
- method public androidx.slice.builders.GridBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+ method public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+ method public androidx.slice.builders.GridRowBuilder addCell(java.util.function.Consumer<androidx.slice.builders.GridRowBuilder.CellBuilder>);
+ method public androidx.slice.builders.GridRowBuilder addSeeMoreAction(android.app.PendingIntent);
+ method public androidx.slice.builders.GridRowBuilder addSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+ method public androidx.slice.builders.GridRowBuilder addSeeMoreCell(java.util.function.Consumer<androidx.slice.builders.GridRowBuilder.CellBuilder>);
+ method public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
field public static final deprecated int ICON_IMAGE = 0; // 0x0
field public static final deprecated int LARGE_IMAGE = 2; // 0x2
field public static final deprecated int SMALL_IMAGE = 1; // 0x1
}
public static final class GridBuilder.CellBuilder extends androidx.slice.builders.TemplateSliceBuilder {
- ctor public GridBuilder.CellBuilder(androidx.slice.builders.GridBuilder);
- ctor public GridBuilder.CellBuilder(androidx.slice.builders.GridBuilder, android.net.Uri);
- method public deprecated androidx.slice.builders.GridBuilder.CellBuilder addImage(android.graphics.drawable.Icon);
- method public deprecated androidx.slice.builders.GridBuilder.CellBuilder addImage(android.graphics.drawable.Icon, boolean);
- method public androidx.slice.builders.GridBuilder.CellBuilder addImage(android.graphics.drawable.Icon, int);
- method public androidx.slice.builders.GridBuilder.CellBuilder addImage(android.graphics.drawable.Icon, int, boolean);
- method public deprecated androidx.slice.builders.GridBuilder.CellBuilder addLargeImage(android.graphics.drawable.Icon);
- method public deprecated androidx.slice.builders.GridBuilder.CellBuilder addLargeImage(android.graphics.drawable.Icon, boolean);
- method public androidx.slice.builders.GridBuilder.CellBuilder addText(java.lang.CharSequence);
- method public androidx.slice.builders.GridBuilder.CellBuilder addText(java.lang.CharSequence, boolean);
- method public androidx.slice.builders.GridBuilder.CellBuilder addTitleText(java.lang.CharSequence);
- method public androidx.slice.builders.GridBuilder.CellBuilder addTitleText(java.lang.CharSequence, boolean);
- method public androidx.slice.builders.GridBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
+ ctor public GridBuilder.CellBuilder(androidx.slice.builders.GridRowBuilder);
+ ctor public GridBuilder.CellBuilder(androidx.slice.builders.GridRowBuilder, android.net.Uri);
+ method public deprecated androidx.slice.builders.GridRowBuilder.CellBuilder addImage(android.graphics.drawable.Icon);
+ method public deprecated androidx.slice.builders.GridRowBuilder.CellBuilder addImage(android.graphics.drawable.Icon, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(android.graphics.drawable.Icon, int);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(android.graphics.drawable.Icon, int, boolean);
+ method public deprecated androidx.slice.builders.GridRowBuilder.CellBuilder addLargeImage(android.graphics.drawable.Icon);
+ method public deprecated androidx.slice.builders.GridRowBuilder.CellBuilder addLargeImage(android.graphics.drawable.Icon, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(java.lang.CharSequence);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(java.lang.CharSequence, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(java.lang.CharSequence);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(java.lang.CharSequence, boolean);
+ method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
}
public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
ctor public ListBuilder(android.content.Context, android.net.Uri);
method public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
- method public androidx.slice.builders.ListBuilder addGrid(androidx.slice.builders.GridBuilder);
- method public androidx.slice.builders.ListBuilder addGrid(java.util.function.Consumer<androidx.slice.builders.GridBuilder>);
+ method public androidx.slice.builders.ListBuilder addGrid(androidx.slice.builders.GridRowBuilder);
+ method public androidx.slice.builders.ListBuilder addGrid(java.util.function.Consumer<androidx.slice.builders.GridRowBuilder>);
method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
method public androidx.slice.builders.ListBuilder addInputRange(java.util.function.Consumer<androidx.slice.builders.ListBuilder.InputRangeBuilder>);
method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
diff --git a/slices/builders/src/main/java/androidx/slice/builders/GridBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/GridBuilder.java
index 488d925..87e7ddb 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/GridBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/GridBuilder.java
@@ -23,14 +23,14 @@
import android.net.Uri;
import android.os.Build;
-import java.util.function.Consumer;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.slice.builders.impl.TemplateBuilderImpl;
+import java.util.function.Consumer;
+
/**
* Builder to construct a row of slice content in a grid format.
@@ -38,10 +38,12 @@
* A grid row is composed of cells, each cell can have a combination of text and images. For more
* details see {@link CellBuilder}.
* </p>
+ * @deprecated TO BE REMOVED; please use {@link GridRowBuilder} instead.
*/
+@Deprecated
public class GridBuilder extends TemplateSliceBuilder {
- private androidx.slice.builders.impl.GridBuilder mImpl;
+ private androidx.slice.builders.impl.GridRowBuilder mImpl;
private boolean mHasSeeMore;
/**
@@ -76,7 +78,7 @@
@Override
void setImpl(TemplateBuilderImpl impl) {
- mImpl = (androidx.slice.builders.impl.GridBuilder) impl;
+ mImpl = (androidx.slice.builders.impl.GridRowBuilder) impl;
}
/**
@@ -190,7 +192,7 @@
* @hide
*/
@RestrictTo(LIBRARY)
- public androidx.slice.builders.impl.GridBuilder getImpl() {
+ public androidx.slice.builders.impl.GridRowBuilder getImpl() {
return mImpl;
}
@@ -210,16 +212,19 @@
*
* A cell can have at most two text items and one image.
* </p>
+ *
+ * @deprecated TO BE REMOVED; please use {@link GridRowBuilder.CellBuilder} instead.
*/
+ @Deprecated
public static final class CellBuilder extends TemplateSliceBuilder {
- private androidx.slice.builders.impl.GridBuilder.CellBuilder mImpl;
+ private androidx.slice.builders.impl.GridRowBuilder.CellBuilder mImpl;
/**
* Create a builder which will construct a slice displayed as a cell in a grid.
* @param parent The builder constructing the parent slice.
*/
public CellBuilder(@NonNull GridBuilder parent) {
- super(parent.mImpl.createGridBuilder());
+ super(parent.mImpl.createGridRowBuilder());
}
/**
@@ -227,12 +232,12 @@
* @param uri Uri to tag for this slice.
*/
public CellBuilder(@NonNull GridBuilder parent, @NonNull Uri uri) {
- super(parent.mImpl.createGridBuilder(uri));
+ super(parent.mImpl.createGridRowBuilder(uri));
}
@Override
void setImpl(TemplateBuilderImpl impl) {
- mImpl = (androidx.slice.builders.impl.GridBuilder.CellBuilder) impl;
+ mImpl = (androidx.slice.builders.impl.GridRowBuilder.CellBuilder) impl;
}
/**
diff --git a/slices/builders/src/main/java/androidx/slice/builders/GridRowBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
new file mode 100644
index 0000000..e8b8836
--- /dev/null
+++ b/slices/builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 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 androidx.slice.builders;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.slice.builders.impl.TemplateBuilderImpl;
+
+import java.util.function.Consumer;
+
+
+/**
+ * Builder to construct a row of slice content in a grid format.
+ * <p>
+ * A grid row is composed of cells, each cell can have a combination of text and images. For more
+ * details see {@link CellBuilder}.
+ * </p>
+ */
+public class GridRowBuilder extends TemplateSliceBuilder {
+
+ private androidx.slice.builders.impl.GridRowBuilder mImpl;
+ private boolean mHasSeeMore;
+
+ /**
+ * Create a builder which will construct a slice displayed in a grid format.
+ * @param parent The builder constructing the parent slice.
+ */
+ public GridRowBuilder(@NonNull ListBuilder parent) {
+ super(parent.getImpl().createGridBuilder());
+ }
+
+ @Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (androidx.slice.builders.impl.GridRowBuilder) impl;
+ }
+
+ /**
+ * Add a cell to the grid builder.
+ */
+ @NonNull
+ public GridRowBuilder addCell(@NonNull CellBuilder builder) {
+ mImpl.addCell((TemplateBuilderImpl) builder.mImpl);
+ return this;
+ }
+
+ /**
+ * Add a cell to the grid builder.
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ @NonNull
+ public GridRowBuilder addCell(@NonNull Consumer<CellBuilder> c) {
+ CellBuilder b = new CellBuilder(this);
+ c.accept(b);
+ return addCell(b);
+ }
+
+ /**
+ * If all content in a slice cannot be shown, the cell added here may be displayed where the
+ * content is cut off.
+ * <p>
+ * This method should only be used if you want to display a custom cell to indicate more
+ * content, consider using {@link #addSeeMoreAction(PendingIntent)} otherwise. If you do
+ * choose to specify a custom cell, the cell should have
+ * {@link CellBuilder#setContentIntent(PendingIntent)} specified to take the user to an
+ * activity to see all of the content.
+ * </p>
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @NonNull
+ public GridRowBuilder addSeeMoreCell(@NonNull CellBuilder builder) {
+ if (mHasSeeMore) {
+ throw new IllegalStateException("Trying to add see more cell when one has "
+ + "already been added");
+ }
+ mImpl.addSeeMoreCell((TemplateBuilderImpl) builder.mImpl);
+ mHasSeeMore = true;
+ return this;
+ }
+
+ /**
+ * If all content in a slice cannot be shown, the cell added here may be displayed where the
+ * content is cut off.
+ * <p>
+ * This method should only be used if you want to display a custom cell to indicate more
+ * content, consider using {@link #addSeeMoreAction(PendingIntent)} otherwise. If you do
+ * choose to specify a custom cell, the cell should have
+ * {@link CellBuilder#setContentIntent(PendingIntent)} specified to take the user to an
+ * activity to see all of the content.
+ * </p>
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ @NonNull
+ public GridRowBuilder addSeeMoreCell(@NonNull Consumer<CellBuilder> c) {
+ CellBuilder b = new CellBuilder(this);
+ c.accept(b);
+ return addSeeMoreCell(b);
+ }
+
+ /**
+ * If all content in a slice cannot be shown, a "see more" affordance may be displayed where
+ * the content is cut off. The action added here should take the user to an activity to see
+ * all of the content, and will be invoked when the "see more" affordance is tapped.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ @NonNull
+ public GridRowBuilder addSeeMoreAction(@NonNull PendingIntent intent) {
+ if (mHasSeeMore) {
+ throw new IllegalStateException("Trying to add see more action when one has "
+ + "already been added");
+ }
+ mImpl.addSeeMoreAction(intent);
+ mHasSeeMore = true;
+ return this;
+ }
+
+ /**
+ * Sets the intent to send when the slice is activated.
+ */
+ @NonNull
+ public GridRowBuilder setPrimaryAction(@NonNull SliceAction action) {
+ mImpl.setPrimaryAction(action);
+ return this;
+ }
+
+ /**
+ * Sets the content description for the entire grid row.
+ */
+ @NonNull
+ public GridRowBuilder setContentDescription(@NonNull CharSequence description) {
+ mImpl.setContentDescription(description);
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public androidx.slice.builders.impl.GridRowBuilder getImpl() {
+ return mImpl;
+ }
+
+ /**
+ * Sub-builder to construct a cell to be displayed in a grid.
+ * <p>
+ * Content added to a cell will be displayed in order vertically, for example the below code
+ * would construct a cell with "First text", and image below it, and then "Second text" below
+ * the image.
+ *
+ * <pre class="prettyprint">
+ * CellBuilder cb = new CellBuilder(parent, sliceUri);
+ * cb.addText("First text")
+ * .addImage(middleIcon)
+ * .addText("Second text");
+ * </pre>
+ *
+ * A cell can have at most two text items and one image.
+ * </p>
+ */
+ public static final class CellBuilder extends TemplateSliceBuilder {
+ private androidx.slice.builders.impl.GridRowBuilder.CellBuilder mImpl;
+
+ /**
+ * Create a builder which will construct a slice displayed as a cell in a grid.
+ * @param parent The builder constructing the parent slice.
+ */
+ public CellBuilder(@NonNull GridRowBuilder parent) {
+ super(parent.mImpl.createGridRowBuilder());
+ }
+
+ /**
+ * Create a builder which will construct a slice displayed as a cell in a grid.
+ * @param uri Uri to tag for this slice.
+ */
+ public CellBuilder(@NonNull GridRowBuilder parent, @NonNull Uri uri) {
+ super(parent.mImpl.createGridRowBuilder(uri));
+ }
+
+ @Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (androidx.slice.builders.impl.GridRowBuilder.CellBuilder) impl;
+ }
+
+ /**
+ * Adds text to the cell. There can be at most two text items, the first two added
+ * will be used, others will be ignored.
+ */
+ @NonNull
+ public CellBuilder addText(@NonNull CharSequence text) {
+ return addText(text, false /* isLoading */);
+ }
+
+ /**
+ * Adds text to the cell. There can be at most two text items, the first two added
+ * will be used, others will be ignored.
+ * <p>
+ * Use this method to specify content that will appear in the template once it's been
+ * loaded.
+ * </p>
+ * @param isLoading indicates whether the app is doing work to load the added content in the
+ * background or not.
+ */
+ @NonNull
+ public CellBuilder addText(@Nullable CharSequence text, boolean isLoading) {
+ mImpl.addText(text, isLoading);
+ return this;
+ }
+
+ /**
+ * Adds text to the cell. Text added with this method will be styled as a title.
+ * There can be at most two text items, the first two added will be used, others
+ * will be ignored.
+ */
+ @NonNull
+ public CellBuilder addTitleText(@NonNull CharSequence text) {
+ return addTitleText(text, false /* isLoading */);
+ }
+
+ /**
+ * Adds text to the cell. Text added with this method will be styled as a title.
+ * There can be at most two text items, the first two added will be used, others
+ * will be ignored.
+ * <p>
+ * Use this method to specify content that will appear in the template once it's been
+ * loaded.
+ * </p>
+ * @param isLoading indicates whether the app is doing work to load the added content in the
+ * background or not.
+ */
+ @NonNull
+ public CellBuilder addTitleText(@Nullable CharSequence text, boolean isLoading) {
+ mImpl.addTitleText(text, isLoading);
+ return this;
+ }
+
+ /**
+ * Adds an image to the cell. There can be at most one image, the first one added will be
+ * used, others will be ignored.
+ *
+ * @param image the image to display in the cell.
+ * @param imageMode the mode that image should be displayed in.
+ *
+ * @see ListBuilder#ICON_IMAGE
+ * @see ListBuilder#SMALL_IMAGE
+ * @see ListBuilder#LARGE_IMAGE
+ */
+ @NonNull
+ public CellBuilder addImage(@NonNull Icon image, @ListBuilder.ImageMode int imageMode) {
+ return addImage(image, imageMode, false /* isLoading */);
+ }
+
+ /**
+ * Adds an image to the cell. There can be at most one image, the first one added will be
+ * used, others will be ignored.
+ * <p>
+ * Use this method to specify content that will appear in the template once it's been
+ * loaded.
+ * </p>
+ * @param image the image to display in the cell.
+ * @param imageMode the mode that image should be displayed in.
+ * @param isLoading indicates whether the app is doing work to load the added content in the
+ * background or not.
+ *
+ * @see ListBuilder#ICON_IMAGE
+ * @see ListBuilder#SMALL_IMAGE
+ * @see ListBuilder#LARGE_IMAGE
+ */
+ @NonNull
+ public CellBuilder addImage(@Nullable Icon image, @ListBuilder.ImageMode int imageMode,
+ boolean isLoading) {
+ mImpl.addImage(image, imageMode, isLoading);
+ return this;
+ }
+
+ /**
+ * Sets the action to be invoked if the user taps on this cell in the row.
+ */
+ @NonNull
+ public CellBuilder setContentIntent(@NonNull PendingIntent intent) {
+ mImpl.setContentIntent(intent);
+ return this;
+ }
+
+ /**
+ * Sets the content description for this cell.
+ */
+ @NonNull
+ public CellBuilder setContentDescription(@NonNull CharSequence description) {
+ mImpl.setContentDescription(description);
+ return this;
+ }
+ }
+}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
index 781b2bd..bec2af9 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
@@ -25,9 +25,6 @@
import android.net.Uri;
import android.os.Build;
-import java.util.List;
-import java.util.function.Consumer;
-
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
@@ -39,6 +36,9 @@
import androidx.slice.builders.impl.ListBuilderV1Impl;
import androidx.slice.builders.impl.TemplateBuilderImpl;
+import java.util.List;
+import java.util.function.Consumer;
+
/**
* Builder to construct slice content in a list format.
* <p>
@@ -120,10 +120,36 @@
/**
* Add a grid row to the list builder.
+ *
+ * @deprecated TO BE REMOVED; use {@link #addGridRow(GridRowBuilder)} instead
*/
@NonNull
+ @Deprecated
public ListBuilder addGrid(@NonNull GridBuilder builder) {
- mImpl.addGrid((TemplateBuilderImpl) builder.getImpl());
+ mImpl.addGridRow((TemplateBuilderImpl) builder.getImpl());
+ return this;
+ }
+
+ /**
+ * Add a grid row to the list builder.
+ *
+ * @deprecated TO BE REMOVED; use {@link #addGridRow(GridRowBuilder)} instead
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ @Deprecated
+ public ListBuilder addGrid(@NonNull Consumer<GridBuilder> c) {
+ GridBuilder b = new GridBuilder(this);
+ c.accept(b);
+ return addGrid(b);
+ }
+
+ /**
+ * Add a grid row to the list builder.
+ */
+ @NonNull
+ public ListBuilder addGridRow(@NonNull GridRowBuilder builder) {
+ mImpl.addGridRow((TemplateBuilderImpl) builder.getImpl());
return this;
}
@@ -132,10 +158,10 @@
*/
@RequiresApi(api = Build.VERSION_CODES.N)
@NonNull
- public ListBuilder addGrid(@NonNull Consumer<GridBuilder> c) {
- GridBuilder b = new GridBuilder(this);
+ public ListBuilder addGridRow(@NonNull Consumer<GridRowBuilder> c) {
+ GridRowBuilder b = new GridRowBuilder(this);
c.accept(b);
- return addGrid(b);
+ return addGridRow(b);
}
/**
diff --git a/slices/builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
index 0d4fa46..f3878be 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
@@ -20,19 +20,20 @@
import android.content.Context;
import android.net.Uri;
-import androidx.annotation.RestrictTo;
import android.util.Log;
import android.util.Pair;
-import java.util.Arrays;
-import java.util.List;
-
+import androidx.annotation.RestrictTo;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
import androidx.slice.SliceSpec;
import androidx.slice.SliceSpecs;
import androidx.slice.builders.impl.TemplateBuilderImpl;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
/**
* Base class of builders of various template types.
*/
@@ -131,7 +132,7 @@
private List<SliceSpec> getSpecs() {
if (SliceProvider.getCurrentSpecs() != null) {
- return SliceProvider.getCurrentSpecs();
+ return new ArrayList<>(SliceProvider.getCurrentSpecs());
}
// TODO: Support getting specs from pinned info.
Log.w(TAG, "Not currently bunding a slice");
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilder.java
deleted file mode 100644
index 99d5ce4..0000000
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilder.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2018 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 androidx.slice.builders.impl;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
-import android.app.PendingIntent;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.slice.builders.SliceAction;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY)
-public interface GridBuilder {
- /**
- * Create an TemplateBuilderImpl that implements {@link CellBuilder}.
- */
- TemplateBuilderImpl createGridBuilder();
-
- /**
- * Create an TemplateBuilderImpl that implements {@link CellBuilder} with the specified Uri.
- */
- TemplateBuilderImpl createGridBuilder(Uri uri);
-
- /**
- * Add a cell to this builder. Expected to be a builder from {@link #createGridBuilder}.
- */
- void addCell(TemplateBuilderImpl impl);
-
- /**
- * If all content in a slice cannot be shown, the cell added here will be displayed where the
- * content is cut off. This cell should have an affordance to take the user to an activity to
- * see all of the content. Expected to be a builder from {@link #createGridBuilder}.
- * <p>
- * Only one see more affordance can be added, this throws {@link IllegalStateException} if
- * a row or action has been previously added.
- * </p>
- */
- void addSeeMoreCell(TemplateBuilderImpl impl);
-
- /**
- * If all content in a slice cannot be shown, a "see more" affordance will be displayed where
- * the content is cut off. The action added here should take the user to an activity to see
- * all of the content, and will be invoked when the "see more" affordance is tapped.
- * <p>
- * Only one see more affordance can be added, this throws {@link IllegalStateException} if
- * a row or action has been previously added.
- * </p>
- */
- void addSeeMoreAction(PendingIntent intent);
-
- /**
- * Sets the action to be invoked if the user taps on the main content of the template.
- */
- void setPrimaryAction(SliceAction action);
-
- /**
- * Sets the content description for the entire grid row.
- */
- void setContentDescription(CharSequence description);
-
- /**
- */
- interface CellBuilder {
- /**
- * Adds text to the cell. There can be at most two text items, the first two added
- * will be used, others will be ignored.
- */
- @NonNull
- void addText(@NonNull CharSequence text);
-
- /**
- * Adds text to the cell. There can be at most two text items, the first two added
- * will be used, others will be ignored.
- * <p>
- * When set to true, the parameter {@code isLoading} indicates that the app is doing work
- * to load this content in the background, in this case the template displays a placeholder
- * until updated.
- */
- @NonNull
- void addText(@Nullable CharSequence text, boolean isLoading);
-
- /**
- * Adds text to the cell. Text added with this method will be styled as a title.
- * There can be at most two text items, the first two added will be used, others
- * will be ignored.
- */
- @NonNull
- void addTitleText(@NonNull CharSequence text);
-
- /**
- * Adds text to the cell. Text added with this method will be styled as a title.
- * There can be at most two text items, the first two added will be used, others
- * will be ignored.
- * <p>
- * When set to true, the parameter {@code isLoading} indicates that the app is doing work
- * to load this content in the background, in this case the template displays a placeholder
- * until updated.
- */
- @NonNull
- void addTitleText(@Nullable CharSequence text, boolean isLoading);
-
- /**
- * Adds an image to the cell. There can be at most one image, the first one added
- * will be used, others will be ignored.
- *
- * @param image the image to display in the cell.
- * @param imageMode the mode that image should be displayed in.
- */
- @NonNull
- void addImage(@NonNull Icon image, int imageMode);
-
- /**
- * Adds an image to the cell. There can be at most one image, the first one added
- * will be used, others will be ignored.
- * <p>
- * When set to true, the parameter {@code isLoading} indicates that the app is doing work
- * to load this content in the background, in this case the template displays a placeholder
- * until updated.l.
- */
- @NonNull
- void addImage(@NonNull Icon image, int imageMode, boolean isLoading);
-
- /**
- * Sets the action to be invoked if the user taps on this cell in the row.
- */
- @NonNull
- void setContentIntent(@NonNull PendingIntent intent);
-
- /**
- * Sets the content description for this cell.
- */
- void setContentDescription(CharSequence description);
- }
-}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilderBasicImpl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilderBasicImpl.java
deleted file mode 100644
index 96df99e..0000000
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilderBasicImpl.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright 2018 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 androidx.slice.builders.impl;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
-import android.app.PendingIntent;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
-import androidx.slice.builders.SliceAction;
-
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY)
-public class GridBuilderBasicImpl extends TemplateBuilderImpl implements GridBuilder {
-
- /**
- */
- public GridBuilderBasicImpl(@NonNull ListBuilderBasicImpl parent) {
- super(parent.createChildBuilder(), null);
- }
-
- /**
- */
- @Override
- public TemplateBuilderImpl createGridBuilder() {
- return new CellBuilder(this);
- }
-
- /**
- */
- @Override
- public TemplateBuilderImpl createGridBuilder(Uri uri) {
- return new CellBuilder(uri);
- }
-
- /**
- */
- @Override
- public void addCell(TemplateBuilderImpl impl) {
- // TODO: Consider extracting some grid content for the basic version.
- }
-
- /**
- */
- @Override
- public void addSeeMoreCell(TemplateBuilderImpl impl) {
- }
-
- /**
- */
- @Override
- public void addSeeMoreAction(PendingIntent intent) {
- }
-
- /**
- */
- @Override
- public void setPrimaryAction(SliceAction action) {
- }
-
- /**
- */
- @Override
- public void setContentDescription(CharSequence description) {
- }
-
- /**
- */
- @Override
- public void apply(Slice.Builder builder) {
-
- }
-
- /**
- */
- public static final class CellBuilder extends TemplateBuilderImpl implements
- GridBuilder.CellBuilder {
-
- /**
- */
- public CellBuilder(@NonNull GridBuilderBasicImpl parent) {
- super(parent.createChildBuilder(), null);
- }
-
- /**
- */
- public CellBuilder(@NonNull Uri uri) {
- super(new Slice.Builder(uri), null);
- }
-
- /**
- */
- @NonNull
- @Override
- public void addText(@NonNull CharSequence text) {
- }
-
- /**
- */
- @Override
- public void addText(@Nullable CharSequence text, boolean isLoading) {
- }
-
- /**
- */
- @NonNull
- @Override
- public void addTitleText(@NonNull CharSequence text) {
- }
-
- /**
- */
- @NonNull
- @Override
- public void addTitleText(@Nullable CharSequence text, boolean isLoading) {
- }
-
- /**
- */
- @NonNull
- @Override
- public void addImage(@NonNull Icon image, int imageMode) {
- }
-
- /**
- */
- @NonNull
- @Override
- public void addImage(@Nullable Icon image, int imageMode, boolean isLoading) {
- }
-
- /**
- */
- @NonNull
- @Override
- public void setContentIntent(@NonNull PendingIntent intent) {
- }
-
- /**
- */
- @Override
- public void setContentDescription(CharSequence description) {
- }
-
- /**
- */
- @Override
- public void apply(Slice.Builder builder) {
-
- }
- }
-}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilderListV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilderListV1Impl.java
deleted file mode 100644
index 8dc89fe..0000000
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/GridBuilderListV1Impl.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright 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 androidx.slice.builders.impl;
-
-import static android.app.slice.Slice.HINT_HORIZONTAL;
-import static android.app.slice.Slice.HINT_LARGE;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
-import static android.app.slice.Slice.HINT_NO_TINT;
-import static android.app.slice.Slice.HINT_PARTIAL;
-import static android.app.slice.Slice.HINT_SEE_MORE;
-import static android.app.slice.Slice.HINT_SHORTCUT;
-import static android.app.slice.Slice.HINT_TITLE;
-import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
-import static androidx.slice.builders.ListBuilder.LARGE_IMAGE;
-
-import android.app.PendingIntent;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-
-import java.util.ArrayList;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
-import androidx.slice.builders.SliceAction;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY)
-public class GridBuilderListV1Impl extends TemplateBuilderImpl implements GridBuilder {
-
- private SliceAction mPrimaryAction;
-
- /**
- */
- public GridBuilderListV1Impl(@NonNull ListBuilderV1Impl parent) {
- super(parent.createChildBuilder(), null);
- }
-
- /**
- */
- @Override
- @NonNull
- public Slice build() {
- Slice.Builder sb = new Slice.Builder(getBuilder())
- .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM);
- sb.addSubSlice(getBuilder().addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build());
- if (mPrimaryAction != null) {
- Slice.Builder actionBuilder = new Slice.Builder(getBuilder())
- .addHints(HINT_SHORTCUT, HINT_TITLE);
- sb.addSubSlice(mPrimaryAction.buildSlice(actionBuilder));
- }
- return sb.build();
- }
-
- /**
- */
- @Override
- public void apply(Slice.Builder builder) {
- }
-
- /**
- */
- @Override
- public TemplateBuilderImpl createGridBuilder() {
- return new CellBuilder(this);
- }
-
- /**
- */
- @Override
- public TemplateBuilderImpl createGridBuilder(Uri uri) {
- return new CellBuilder(uri);
- }
-
- /**
- */
- @Override
- public void addCell(TemplateBuilderImpl builder) {
- getBuilder().addSubSlice(builder.getBuilder().addHints(HINT_LIST_ITEM).build());
- }
-
-
- /**
- */
- @Override
- public void addSeeMoreCell(@NonNull TemplateBuilderImpl builder) {
- builder.getBuilder().addHints(HINT_SEE_MORE);
- getBuilder().addSubSlice(builder.build());
- }
-
- /**
- */
- @Override
- public void addSeeMoreAction(PendingIntent intent) {
- getBuilder().addSubSlice(
- new Slice.Builder(getBuilder())
- .addHints(HINT_SEE_MORE)
- .addAction(intent, new Slice.Builder(getBuilder()).build(), null)
- .build());
- }
-
- /**
- */
- @Override
- public void setPrimaryAction(SliceAction action) {
- mPrimaryAction = action;
- }
-
- /**
- */
- @Override
- public void setContentDescription(CharSequence description) {
- getBuilder().addText(description, SUBTYPE_CONTENT_DESCRIPTION);
- }
-
- /**
- */
- public static final class CellBuilder extends TemplateBuilderImpl implements
- GridBuilder.CellBuilder {
-
- private PendingIntent mContentIntent;
-
- /**
- */
- public CellBuilder(@NonNull GridBuilderListV1Impl parent) {
- super(parent.createChildBuilder(), null);
- }
-
- /**
- */
- public CellBuilder(@NonNull Uri uri) {
- super(new Slice.Builder(uri), null);
- }
-
- /**
- */
- @NonNull
- @Override
- public void addText(@NonNull CharSequence text) {
- addText(text, false /* isLoading */);
- }
-
- /**
- */
- @Override
- public void addText(@Nullable CharSequence text, boolean isLoading) {
- @Slice.SliceHint String[] hints = isLoading
- ? new String[] {HINT_PARTIAL}
- : new String[0];
- getBuilder().addText(text, null, hints);
- }
-
- /**
- */
- @NonNull
- @Override
- public void addTitleText(@NonNull CharSequence text) {
- addTitleText(text, false /* isLoading */);
- }
-
- /**
- */
- @NonNull
- @Override
- public void addTitleText(@Nullable CharSequence text, boolean isLoading) {
- @Slice.SliceHint String[] hints = isLoading
- ? new String[] {HINT_PARTIAL, HINT_LARGE}
- : new String[] {HINT_LARGE};
- getBuilder().addText(text, null, hints);
- }
-
- /**
- */
- @NonNull
- @Override
- public void addImage(@NonNull Icon image, int imageMode) {
- addImage(image, imageMode, false /* isLoading */);
- }
-
- /**
- */
- @NonNull
- @Override
- public void addImage(@Nullable Icon image, int imageMode, boolean isLoading) {
- ArrayList<String> hints = new ArrayList<>();
- if (imageMode != ICON_IMAGE) {
- hints.add(HINT_NO_TINT);
- }
- if (imageMode == LARGE_IMAGE) {
- hints.add(HINT_LARGE);
- }
- if (isLoading) {
- hints.add(HINT_PARTIAL);
- }
- getBuilder().addIcon(image, null, hints);
- }
-
- /**
- */
- @NonNull
- @Override
- public void setContentIntent(@NonNull PendingIntent intent) {
- mContentIntent = intent;
- }
-
- /**
- */
- @Override
- public void setContentDescription(CharSequence description) {
- getBuilder().addText(description, SUBTYPE_CONTENT_DESCRIPTION);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- @Override
- public void apply(Slice.Builder b) {
- }
-
- /**
- */
- @Override
- @NonNull
- public Slice build() {
- if (mContentIntent != null) {
- return new Slice.Builder(getBuilder())
- .addHints(HINT_HORIZONTAL)
- .addAction(mContentIntent, getBuilder().build(), null)
- .build();
- }
- return getBuilder().addHints(HINT_HORIZONTAL).build();
- }
- }
-}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilder.java
new file mode 100644
index 0000000..b5eefff
--- /dev/null
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilder.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018 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 androidx.slice.builders.impl;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.slice.builders.SliceAction;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public interface GridRowBuilder {
+ /**
+ * Create an TemplateBuilderImpl that implements {@link CellBuilder}.
+ */
+ TemplateBuilderImpl createGridRowBuilder();
+
+ /**
+ * Create an TemplateBuilderImpl that implements {@link CellBuilder} with the specified Uri.
+ */
+ TemplateBuilderImpl createGridRowBuilder(Uri uri);
+
+ /**
+ * Add a cell to this builder. Expected to be a builder from {@link #createGridRowBuilder}.
+ */
+ void addCell(TemplateBuilderImpl impl);
+
+ /**
+ * If all content in a slice cannot be shown, the cell added here will be displayed where the
+ * content is cut off. This cell should have an affordance to take the user to an activity to
+ * see all of the content. Expected to be a builder from {@link #createGridRowBuilder}.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ void addSeeMoreCell(TemplateBuilderImpl impl);
+
+ /**
+ * If all content in a slice cannot be shown, a "see more" affordance will be displayed where
+ * the content is cut off. The action added here should take the user to an activity to see
+ * all of the content, and will be invoked when the "see more" affordance is tapped.
+ * <p>
+ * Only one see more affordance can be added, this throws {@link IllegalStateException} if
+ * a row or action has been previously added.
+ * </p>
+ */
+ void addSeeMoreAction(PendingIntent intent);
+
+ /**
+ * Sets the action to be invoked if the user taps on the main content of the template.
+ */
+ void setPrimaryAction(SliceAction action);
+
+ /**
+ * Sets the content description for the entire grid row.
+ */
+ void setContentDescription(CharSequence description);
+
+ /**
+ */
+ interface CellBuilder {
+ /**
+ * Adds text to the cell. There can be at most two text items, the first two added
+ * will be used, others will be ignored.
+ */
+ @NonNull
+ void addText(@NonNull CharSequence text);
+
+ /**
+ * Adds text to the cell. There can be at most two text items, the first two added
+ * will be used, others will be ignored.
+ * <p>
+ * When set to true, the parameter {@code isLoading} indicates that the app is doing work
+ * to load this content in the background, in this case the template displays a placeholder
+ * until updated.
+ */
+ @NonNull
+ void addText(@Nullable CharSequence text, boolean isLoading);
+
+ /**
+ * Adds text to the cell. Text added with this method will be styled as a title.
+ * There can be at most two text items, the first two added will be used, others
+ * will be ignored.
+ */
+ @NonNull
+ void addTitleText(@NonNull CharSequence text);
+
+ /**
+ * Adds text to the cell. Text added with this method will be styled as a title.
+ * There can be at most two text items, the first two added will be used, others
+ * will be ignored.
+ * <p>
+ * When set to true, the parameter {@code isLoading} indicates that the app is doing work
+ * to load this content in the background, in this case the template displays a placeholder
+ * until updated.
+ */
+ @NonNull
+ void addTitleText(@Nullable CharSequence text, boolean isLoading);
+
+ /**
+ * Adds an image to the cell. There can be at most one image, the first one added
+ * will be used, others will be ignored.
+ *
+ * @param image the image to display in the cell.
+ * @param imageMode the mode that image should be displayed in.
+ */
+ @NonNull
+ void addImage(@NonNull Icon image, int imageMode);
+
+ /**
+ * Adds an image to the cell. There can be at most one image, the first one added
+ * will be used, others will be ignored.
+ * <p>
+ * When set to true, the parameter {@code isLoading} indicates that the app is doing work
+ * to load this content in the background, in this case the template displays a placeholder
+ * until updated.l.
+ */
+ @NonNull
+ void addImage(@NonNull Icon image, int imageMode, boolean isLoading);
+
+ /**
+ * Sets the action to be invoked if the user taps on this cell in the row.
+ */
+ @NonNull
+ void setContentIntent(@NonNull PendingIntent intent);
+
+ /**
+ * Sets the content description for this cell.
+ */
+ void setContentDescription(CharSequence description);
+ }
+}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderBasicImpl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderBasicImpl.java
new file mode 100644
index 0000000..dcd8791
--- /dev/null
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderBasicImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018 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 androidx.slice.builders.impl;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.slice.Slice;
+import androidx.slice.builders.SliceAction;
+
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class GridRowBuilderBasicImpl extends TemplateBuilderImpl implements GridRowBuilder {
+
+ /**
+ */
+ public GridRowBuilderBasicImpl(@NonNull ListBuilderBasicImpl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridRowBuilder() {
+ return new CellBuilder(this);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridRowBuilder(Uri uri) {
+ return new CellBuilder(uri);
+ }
+
+ /**
+ */
+ @Override
+ public void addCell(TemplateBuilderImpl impl) {
+ // TODO: Consider extracting some grid content for the basic version.
+ }
+
+ /**
+ */
+ @Override
+ public void addSeeMoreCell(TemplateBuilderImpl impl) {
+ }
+
+ /**
+ */
+ @Override
+ public void addSeeMoreAction(PendingIntent intent) {
+ }
+
+ /**
+ */
+ @Override
+ public void setPrimaryAction(SliceAction action) {
+ }
+
+ /**
+ */
+ @Override
+ public void setContentDescription(CharSequence description) {
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+
+ /**
+ */
+ public static final class CellBuilder extends TemplateBuilderImpl implements
+ GridRowBuilder.CellBuilder {
+
+ /**
+ */
+ public CellBuilder(@NonNull GridRowBuilderBasicImpl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public CellBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri), null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addText(@NonNull CharSequence text) {
+ }
+
+ /**
+ */
+ @Override
+ public void addText(@Nullable CharSequence text, boolean isLoading) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addTitleText(@NonNull CharSequence text) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addTitleText(@Nullable CharSequence text, boolean isLoading) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addImage(@NonNull Icon image, int imageMode) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addImage(@Nullable Icon image, int imageMode, boolean isLoading) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setContentIntent(@NonNull PendingIntent intent) {
+ }
+
+ /**
+ */
+ @Override
+ public void setContentDescription(CharSequence description) {
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+ }
+}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
new file mode 100644
index 0000000..0c077ba
--- /dev/null
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 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 androidx.slice.builders.impl;
+
+import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static android.app.slice.Slice.HINT_LARGE;
+import static android.app.slice.Slice.HINT_LIST_ITEM;
+import static android.app.slice.Slice.HINT_NO_TINT;
+import static android.app.slice.Slice.HINT_PARTIAL;
+import static android.app.slice.Slice.HINT_SEE_MORE;
+import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
+import static androidx.slice.builders.ListBuilder.LARGE_IMAGE;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.slice.Slice;
+import androidx.slice.builders.SliceAction;
+
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class GridRowBuilderListV1Impl extends TemplateBuilderImpl implements GridRowBuilder {
+
+ private SliceAction mPrimaryAction;
+
+ /**
+ */
+ public GridRowBuilderListV1Impl(@NonNull ListBuilderV1Impl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ @Override
+ @NonNull
+ public Slice build() {
+ Slice.Builder sb = new Slice.Builder(getBuilder())
+ .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM);
+ sb.addSubSlice(getBuilder().addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build());
+ if (mPrimaryAction != null) {
+ Slice.Builder actionBuilder = new Slice.Builder(getBuilder())
+ .addHints(HINT_SHORTCUT, HINT_TITLE);
+ sb.addSubSlice(mPrimaryAction.buildSlice(actionBuilder));
+ }
+ return sb.build();
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridRowBuilder() {
+ return new CellBuilder(this);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridRowBuilder(Uri uri) {
+ return new CellBuilder(uri);
+ }
+
+ /**
+ */
+ @Override
+ public void addCell(TemplateBuilderImpl builder) {
+ getBuilder().addSubSlice(builder.getBuilder().addHints(HINT_LIST_ITEM).build());
+ }
+
+
+ /**
+ */
+ @Override
+ public void addSeeMoreCell(@NonNull TemplateBuilderImpl builder) {
+ builder.getBuilder().addHints(HINT_SEE_MORE);
+ getBuilder().addSubSlice(builder.build());
+ }
+
+ /**
+ */
+ @Override
+ public void addSeeMoreAction(PendingIntent intent) {
+ getBuilder().addSubSlice(
+ new Slice.Builder(getBuilder())
+ .addHints(HINT_SEE_MORE)
+ .addAction(intent, new Slice.Builder(getBuilder()).build(), null)
+ .build());
+ }
+
+ /**
+ */
+ @Override
+ public void setPrimaryAction(SliceAction action) {
+ mPrimaryAction = action;
+ }
+
+ /**
+ */
+ @Override
+ public void setContentDescription(CharSequence description) {
+ getBuilder().addText(description, SUBTYPE_CONTENT_DESCRIPTION);
+ }
+
+ /**
+ */
+ public static final class CellBuilder extends TemplateBuilderImpl implements
+ GridRowBuilder.CellBuilder {
+
+ private PendingIntent mContentIntent;
+
+ /**
+ */
+ public CellBuilder(@NonNull GridRowBuilderListV1Impl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public CellBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri), null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addText(@NonNull CharSequence text) {
+ addText(text, false /* isLoading */);
+ }
+
+ /**
+ */
+ @Override
+ public void addText(@Nullable CharSequence text, boolean isLoading) {
+ @Slice.SliceHint String[] hints = isLoading
+ ? new String[] {HINT_PARTIAL}
+ : new String[0];
+ getBuilder().addText(text, null, hints);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addTitleText(@NonNull CharSequence text) {
+ addTitleText(text, false /* isLoading */);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addTitleText(@Nullable CharSequence text, boolean isLoading) {
+ @Slice.SliceHint String[] hints = isLoading
+ ? new String[] {HINT_PARTIAL, HINT_LARGE}
+ : new String[] {HINT_LARGE};
+ getBuilder().addText(text, null, hints);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addImage(@NonNull Icon image, int imageMode) {
+ addImage(image, imageMode, false /* isLoading */);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addImage(@Nullable Icon image, int imageMode, boolean isLoading) {
+ ArrayList<String> hints = new ArrayList<>();
+ if (imageMode != ICON_IMAGE) {
+ hints.add(HINT_NO_TINT);
+ }
+ if (imageMode == LARGE_IMAGE) {
+ hints.add(HINT_LARGE);
+ }
+ if (isLoading) {
+ hints.add(HINT_PARTIAL);
+ }
+ getBuilder().addIcon(image, null, hints);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setContentIntent(@NonNull PendingIntent intent) {
+ mContentIntent = intent;
+ }
+
+ /**
+ */
+ @Override
+ public void setContentDescription(CharSequence description) {
+ getBuilder().addText(description, SUBTYPE_CONTENT_DESCRIPTION);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @Override
+ public void apply(Slice.Builder b) {
+ }
+
+ /**
+ */
+ @Override
+ @NonNull
+ public Slice build() {
+ if (mContentIntent != null) {
+ return new Slice.Builder(getBuilder())
+ .addHints(HINT_HORIZONTAL)
+ .addAction(mContentIntent, getBuilder().build(), null)
+ .build();
+ }
+ return getBuilder().addHints(HINT_HORIZONTAL).build();
+ }
+ }
+}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
index 386f215..da4fb69 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
@@ -22,13 +22,13 @@
import android.graphics.drawable.Icon;
import android.net.Uri;
-import java.util.List;
-
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.slice.builders.SliceAction;
+import java.util.List;
+
/**
* @hide
*/
@@ -42,7 +42,7 @@
/**
* Add a grid row to the list builder.
*/
- void addGrid(TemplateBuilderImpl impl);
+ void addGridRow(TemplateBuilderImpl impl);
/**
* Adds a header to this template.
@@ -108,7 +108,7 @@
*/
TemplateBuilderImpl createRowBuilder(Uri uri);
/**
- * Create a builder that implements {@link GridBuilder}.
+ * Create a builder that implements {@link GridRowBuilder}.
*/
TemplateBuilderImpl createGridBuilder();
/**
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
index 8da1d96..e9a659d 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
@@ -23,8 +23,6 @@
import android.graphics.drawable.Icon;
import android.net.Uri;
-import java.util.List;
-
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -32,6 +30,8 @@
import androidx.slice.SliceSpec;
import androidx.slice.builders.SliceAction;
+import java.util.List;
+
/**
* @hide
*/
@@ -54,7 +54,7 @@
/**
*/
@Override
- public void addGrid(TemplateBuilderImpl impl) {
+ public void addGridRow(TemplateBuilderImpl impl) {
// Do nothing.
}
@@ -129,7 +129,7 @@
*/
@Override
public TemplateBuilderImpl createGridBuilder() {
- return new GridBuilderBasicImpl(this);
+ return new GridRowBuilderBasicImpl(this);
}
@Override
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
index a9be556..6671024 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
@@ -41,9 +41,6 @@
import android.graphics.drawable.Icon;
import android.net.Uri;
-import java.util.ArrayList;
-import java.util.List;
-
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -52,6 +49,9 @@
import androidx.slice.SliceSpec;
import androidx.slice.builders.SliceAction;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* @hide
*/
@@ -96,7 +96,7 @@
*/
@NonNull
@Override
- public void addGrid(@NonNull TemplateBuilderImpl builder) {
+ public void addGridRow(@NonNull TemplateBuilderImpl builder) {
getBuilder().addSubSlice(builder.build());
}
@@ -280,7 +280,7 @@
*/
@Override
public TemplateBuilderImpl createGridBuilder() {
- return new GridBuilderListV1Impl(this);
+ return new GridRowBuilderListV1Impl(this);
}
/**
diff --git a/slices/core/src/androidTest/java/androidx/slice/SliceTest.java b/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
index ef3b000..563473a 100644
--- a/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
+++ b/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
@@ -44,6 +44,8 @@
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import androidx.slice.core.test.R;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,8 +54,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import androidx.slice.core.test.R;
-
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SliceTest {
@@ -68,7 +68,7 @@
sFlag = false;
Slice.bindSlice(mContext,
BASE_URI.buildUpon().appendPath("set_flag").build(),
- Collections.<SliceSpec>emptyList());
+ Collections.<SliceSpec>emptySet());
assertFalse(sFlag);
}
@@ -79,14 +79,14 @@
@Test
public void testSliceUri() {
- Slice s = Slice.bindSlice(mContext, BASE_URI, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, BASE_URI, Collections.<SliceSpec>emptySet());
assertEquals(BASE_URI, s.getUri());
}
@Test
public void testSubSlice() {
Uri uri = BASE_URI.buildUpon().appendPath("subslice").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -101,7 +101,7 @@
@Test
public void testText() {
Uri uri = BASE_URI.buildUpon().appendPath("text").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -114,7 +114,7 @@
@Test
public void testIcon() {
Uri uri = BASE_URI.buildUpon().appendPath("icon").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -138,7 +138,7 @@
mContext.registerReceiver(receiver,
new IntentFilter(mContext.getPackageName() + ".action"));
Uri uri = BASE_URI.buildUpon().appendPath("action").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -161,7 +161,7 @@
@Test
public void testInt() {
Uri uri = BASE_URI.buildUpon().appendPath("int").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -173,7 +173,7 @@
@Test
public void testTimestamp() {
Uri uri = BASE_URI.buildUpon().appendPath("timestamp").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -187,7 +187,7 @@
// Note this tests that hints are propagated through to the client but not that any specific
// hints have any effects.
Uri uri = BASE_URI.buildUpon().appendPath("hints").build();
- Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptySet());
assertEquals(uri, s.getUri());
assertEquals(Arrays.asList(HINT_LIST), s.getHints());
diff --git a/slices/core/src/androidTest/java/androidx/slice/compat/CompatPinnedListTest.java b/slices/core/src/androidTest/java/androidx/slice/compat/CompatPinnedListTest.java
index 939309d..7a37396 100644
--- a/slices/core/src/androidTest/java/androidx/slice/compat/CompatPinnedListTest.java
+++ b/slices/core/src/androidTest/java/androidx/slice/compat/CompatPinnedListTest.java
@@ -22,14 +22,17 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
-import static org.junit.Assert.assertArrayEquals;
-
import android.content.Context;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import androidx.collection.ArraySet;
+import androidx.slice.SliceSpec;
+
+import junit.framework.AssertionFailedError;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -37,9 +40,7 @@
import java.util.Arrays;
import java.util.Collections;
-import java.util.List;
-
-import androidx.slice.SliceSpec;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -47,7 +48,7 @@
private final Context mContext = InstrumentationRegistry.getContext();
private CompatPinnedList mCompatPinnedList;
- private List<SliceSpec> mSpecs;
+ private Set<SliceSpec> mSpecs;
private static final SliceSpec[] FIRST_SPECS = new SliceSpec[]{
new SliceSpec("spec1", 3),
@@ -66,7 +67,7 @@
@Before
public void setup() {
mCompatPinnedList = new CompatPinnedList(mContext, "test_file");
- mSpecs = Collections.emptyList();
+ mSpecs = Collections.emptySet();
}
@After
@@ -110,19 +111,31 @@
public void testMergeSpecs() {
Uri uri = Uri.parse("content://something/something");
- assertEquals(Collections.emptyList(), mCompatPinnedList.getSpecs(uri));
+ assertEquals(Collections.emptySet(), mCompatPinnedList.getSpecs(uri));
- mCompatPinnedList.addPin(uri, "my_pkg", Arrays.asList(FIRST_SPECS));
- assertArrayEquals(FIRST_SPECS, mCompatPinnedList.getSpecs(uri).toArray(new SliceSpec[0]));
+ mCompatPinnedList.addPin(uri, "my_pkg", new ArraySet<>(Arrays.asList(FIRST_SPECS)));
+ assertSetEquals(new ArraySet<>(Arrays.asList(FIRST_SPECS)),
+ mCompatPinnedList.getSpecs(uri));
- mCompatPinnedList.addPin(uri, "my_pkg2", Arrays.asList(SECOND_SPECS));
- assertArrayEquals(new SliceSpec[]{
+ mCompatPinnedList.addPin(uri, "my_pkg2", new ArraySet<>(Arrays.asList(SECOND_SPECS)));
+ assertSetEquals(new ArraySet<>(Arrays.asList(new SliceSpec[]{
// spec1 is gone because it's not in the second set.
new SliceSpec("spec2", 1), // spec2 is 1 because it's smaller in the second set.
new SliceSpec("spec3", 2), // spec3 is the same in both sets
new SliceSpec("spec4", 1), // spec4 is 1 because it's smaller in the first set.
// spec5 is gone because it's not in the first set.
- }, mCompatPinnedList.getSpecs(uri).toArray(new SliceSpec[0]));
+ })), mCompatPinnedList.getSpecs(uri));
+ }
+ private <T> void assertSetEquals(Set<T> a, Set<T> b) {
+ if (a.size() != b.size()) {
+ throw new AssertionFailedError("Wanted " + a + " but received " + b);
+ }
+
+ for (T o : a) {
+ if (!b.contains(o)) {
+ throw new AssertionFailedError("Wanted " + a + " but received " + b);
+ }
+ }
}
}
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index 2007e7a..d27d246 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -48,10 +48,6 @@
import android.os.Bundle;
import android.os.Parcelable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -61,6 +57,11 @@
import androidx.core.os.BuildCompat;
import androidx.slice.compat.SliceProviderCompat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
/**
* A slice is a piece of app content and actions that can be surfaced outside of the app.
*
@@ -441,7 +442,7 @@
@SuppressWarnings("NewApi") // Lint doesn't understand BuildCompat.
@Nullable
public static Slice bindSlice(Context context, @NonNull Uri uri,
- List<SliceSpec> supportedSpecs) {
+ Set<SliceSpec> supportedSpecs) {
if (BuildCompat.isAtLeastP()) {
return callBindSlice(context, uri, supportedSpecs);
} else {
@@ -451,8 +452,8 @@
@RequiresApi(28)
private static Slice callBindSlice(Context context, Uri uri,
- List<SliceSpec> supportedSpecs) {
+ Set<SliceSpec> supportedSpecs) {
return SliceConvert.wrap(context.getSystemService(SliceManager.class)
- .bindSlice(uri, unwrap(supportedSpecs)));
+ .bindSlice(uri, new ArrayList<>(unwrap(supportedSpecs))));
}
}
diff --git a/slices/core/src/main/java/androidx/slice/SliceConvert.java b/slices/core/src/main/java/androidx/slice/SliceConvert.java
index 542da98..58531fa 100644
--- a/slices/core/src/main/java/androidx/slice/SliceConvert.java
+++ b/slices/core/src/main/java/androidx/slice/SliceConvert.java
@@ -26,9 +26,10 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
-import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* Convert between {@link androidx.slice.Slice} and {@link android.app.slice.Slice}
@@ -78,9 +79,9 @@
return new android.app.slice.SliceSpec(spec.getType(), spec.getRevision());
}
- static List<android.app.slice.SliceSpec> unwrap(
- List<androidx.slice.SliceSpec> supportedSpecs) {
- List<android.app.slice.SliceSpec> ret = new ArrayList<>();
+ static Set<android.app.slice.SliceSpec> unwrap(
+ Set<androidx.slice.SliceSpec> supportedSpecs) {
+ Set<android.app.slice.SliceSpec> ret = new ArraySet<>();
for (androidx.slice.SliceSpec spec : supportedSpecs) {
ret.add(unwrap(spec));
}
@@ -133,9 +134,9 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- public static List<androidx.slice.SliceSpec> wrap(
+ public static Set<androidx.slice.SliceSpec> wrap(
List<android.app.slice.SliceSpec> supportedSpecs) {
- List<androidx.slice.SliceSpec> ret = new ArrayList<>();
+ Set<androidx.slice.SliceSpec> ret = new ArraySet<>();
for (android.app.slice.SliceSpec spec : supportedSpecs) {
ret.add(wrap(spec));
}
diff --git a/slices/core/src/main/java/androidx/slice/SliceProvider.java b/slices/core/src/main/java/androidx/slice/SliceProvider.java
index b0cd22f..51ffea6 100644
--- a/slices/core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slices/core/src/main/java/androidx/slice/SliceProvider.java
@@ -23,16 +23,16 @@
import android.content.pm.ProviderInfo;
import android.database.ContentObserver;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.core.os.BuildCompat;
-
-import java.util.List;
-
import androidx.slice.compat.ContentProviderWrapper;
import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.compat.SliceProviderWrapperContainer;
+import java.util.Set;
+
/**
* A SliceProvider allows an app to provide content to be displayed in system spaces. This content
* is templated and can contain actions, and the behavior of how it is surfaced is specific to the
@@ -74,7 +74,7 @@
*/
public abstract class SliceProvider extends ContentProviderWrapper {
- private static List<SliceSpec> sSpecs;
+ private static Set<SliceSpec> sSpecs;
@Override
public void attachInfo(Context context, ProviderInfo info) {
@@ -105,12 +105,12 @@
public abstract boolean onCreateSliceProvider();
/**
- * Implemented to create a slice. Will be called on the main thread.
+ * Implemented to create a slice.
* <p>
* onBindSlice should return as quickly as possible so that the UI tied
* to this slice can be responsive. No network or other IO will be allowed
* during onBindSlice. Any loading that needs to be done should happen
- * off the main thread with a call to {@link ContentResolver#notifyChange(Uri, ContentObserver)}
+ * in the background with a call to {@link ContentResolver#notifyChange(Uri, ContentObserver)}
* when the app is ready to provide the complete data in onBindSlice.
* <p>
*
@@ -169,7 +169,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- public static void setSpecs(List<SliceSpec> specs) {
+ public static void setSpecs(Set<SliceSpec> specs) {
sSpecs = specs;
}
@@ -177,7 +177,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static List<SliceSpec> getCurrentSpecs() {
+ public static Set<SliceSpec> getCurrentSpecs() {
return sSpecs;
}
}
diff --git a/slices/core/src/main/java/androidx/slice/SliceSpec.java b/slices/core/src/main/java/androidx/slice/SliceSpec.java
index 7f97769..27a4dfb 100644
--- a/slices/core/src/main/java/androidx/slice/SliceSpec.java
+++ b/slices/core/src/main/java/androidx/slice/SliceSpec.java
@@ -17,6 +17,7 @@
package androidx.slice;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -84,4 +85,14 @@
SliceSpec other = (SliceSpec) obj;
return mType.equals(other.mType) && mRevision == other.mRevision;
}
+
+ @Override
+ public int hashCode() {
+ return mType.hashCode() + Integer.hashCode(mRevision);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SliceSpec{%s,%d}", mType, mRevision);
+ }
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index c8e9032..56b8122 100644
--- a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -20,19 +20,16 @@
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.SystemClock;
+import android.text.TextUtils;
+
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArraySet;
import androidx.core.util.ObjectsCompat;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
import androidx.slice.SliceSpec;
+import java.util.Set;
+
/**
* Tracks the current packages requesting pinning of any given slice. It will clear the
* list after a reboot since the packages are no longer requesting pinning.
@@ -80,18 +77,18 @@
/**
* Get the list of specs for a pinned Uri.
*/
- public synchronized List<SliceSpec> getSpecs(Uri uri) {
- List<SliceSpec> specs = new ArrayList<>();
+ public synchronized ArraySet<SliceSpec> getSpecs(Uri uri) {
+ ArraySet<SliceSpec> specs = new ArraySet<>();
SharedPreferences prefs = getPrefs();
String specNamesStr = prefs.getString(SPEC_NAME_PREFIX + uri.toString(), null);
String specRevsStr = prefs.getString(SPEC_REV_PREFIX + uri.toString(), null);
if (TextUtils.isEmpty(specNamesStr) || TextUtils.isEmpty(specRevsStr)) {
- return Collections.emptyList();
+ return new ArraySet<>();
}
String[] specNames = specNamesStr.split(",");
String[] specRevs = specRevsStr.split(",");
if (specNames.length != specRevs.length) {
- return Collections.emptyList();
+ return new ArraySet<>();
}
for (int i = 0; i < specNames.length; i++) {
specs.add(new SliceSpec(specNames[i], Integer.parseInt(specRevs[i])));
@@ -105,12 +102,12 @@
.commit();
}
- private void setSpecs(Uri uri, List<SliceSpec> specs) {
+ private void setSpecs(Uri uri, ArraySet<SliceSpec> specs) {
String[] specNames = new String[specs.size()];
String[] specRevs = new String[specs.size()];
for (int i = 0; i < specs.size(); i++) {
- specNames[i] = specs.get(i).getType();
- specRevs[i] = String.valueOf(specs.get(i).getRevision());
+ specNames[i] = specs.valueAt(i).getType();
+ specRevs[i] = String.valueOf(specs.valueAt(i).getRevision());
}
getPrefs().edit()
.putString(SPEC_NAME_PREFIX + uri.toString(), TextUtils.join(",", specNames))
@@ -127,13 +124,13 @@
* Adds a pin for a specific uri/pkg pair and returns true if the
* uri was not previously pinned.
*/
- public synchronized boolean addPin(Uri uri, String pkg, List<SliceSpec> specs) {
+ public synchronized boolean addPin(Uri uri, String pkg, Set<SliceSpec> specs) {
Set<String> pins = getPins(uri);
boolean wasNotPinned = pins.isEmpty();
pins.add(pkg);
setPins(uri, pins);
if (wasNotPinned) {
- setSpecs(uri, specs);
+ setSpecs(uri, new ArraySet<>(specs));
} else {
setSpecs(uri, mergeSpecs(getSpecs(uri), specs));
}
@@ -154,21 +151,22 @@
return pins.size() == 0;
}
- private static List<SliceSpec> mergeSpecs(List<SliceSpec> specs,
- List<SliceSpec> supportedSpecs) {
+ private static ArraySet<SliceSpec> mergeSpecs(ArraySet<SliceSpec> specs,
+ Set<SliceSpec> supportedSpecs) {
for (int i = 0; i < specs.size(); i++) {
- SliceSpec s = specs.get(i);
+ SliceSpec s = specs.valueAt(i);
SliceSpec other = findSpec(supportedSpecs, s.getType());
if (other == null) {
- specs.remove(i--);
+ specs.removeAt(i--);
} else if (other.getRevision() < s.getRevision()) {
- specs.set(i, other);
+ specs.removeAt(i--);
+ specs.add(other);
}
}
return specs;
}
- private static SliceSpec findSpec(List<SliceSpec> specs, String type) {
+ private static SliceSpec findSpec(Set<SliceSpec> specs, String type) {
for (SliceSpec spec : specs) {
if (ObjectsCompat.equals(spec.getType(), type)) {
return spec;
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index 1c3e432..07bce69 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -40,22 +40,22 @@
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
-import androidx.annotation.Nullable;
+import android.util.ArraySet;
+import android.util.Log;
+
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.core.util.Preconditions;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
import androidx.slice.SliceSpec;
import androidx.slice.core.R;
import androidx.slice.core.SliceHints;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
/**
* @hide
*/
@@ -80,26 +80,19 @@
public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
private static final String DATA_PREFIX = "slice_data_";
+ private static final long SLICE_BIND_ANR = 2000;
+
private static final boolean DEBUG = false;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private SliceProvider mSliceProvider;
private CompatPinnedList mPinnedList;
- private String mBindingPkg;
+
+ private String mCallback;
public SliceProviderCompat(SliceProvider provider) {
mSliceProvider = provider;
}
- /**
- * Return the package name of the caller that initiated the binding request
- * currently happening. The returned package will have been
- * verified to belong to the calling UID. Returns {@code null} if not
- * currently performing an {@link SliceProvider#onBindSlice(Uri)}.
- */
- public final @Nullable String getBindingPackage() {
- return mBindingPkg;
- }
-
@Override
public boolean onCreate() {
mPinnedList = new CompatPinnedList(getContext(),
@@ -163,7 +156,7 @@
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
"Slice binding requires the permission BIND_SLICE");
}
- List<SliceSpec> specs = getSpecs(extras);
+ Set<SliceSpec> specs = getSpecs(extras);
Slice s = handleBindSlice(uri, specs, getCallingPackage());
Bundle b = new Bundle();
@@ -174,7 +167,7 @@
Uri uri = mSliceProvider.onMapIntentToUri(intent);
Bundle b = new Bundle();
if (uri != null) {
- List<SliceSpec> specs = getSpecs(extras);
+ Set<SliceSpec> specs = getSpecs(extras);
Slice s = handleBindSlice(uri, specs, getCallingPackage());
b.putParcelable(EXTRA_SLICE, s.toBundle());
} else {
@@ -189,7 +182,7 @@
return b;
} else if (method.equals(METHOD_PIN)) {
Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- List<SliceSpec> specs = getSpecs(extras);
+ Set<SliceSpec> specs = getSpecs(extras);
String pkg = extras.getString(EXTRA_PKG);
if (mPinnedList.addPin(uri, pkg, specs)) {
handleSlicePinned(uri);
@@ -212,46 +205,26 @@
}
private void handleSlicePinned(final Uri sliceUri) {
- if (Looper.myLooper() == Looper.getMainLooper()) {
+ mCallback = "onSlicePinned";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
mSliceProvider.onSlicePinned(sliceUri);
- } else {
- final CountDownLatch latch = new CountDownLatch(1);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mSliceProvider.onSlicePinned(sliceUri);
- latch.countDown();
- }
- });
- try {
- latch.await();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
+ } finally {
+ mHandler.removeCallbacks(mAnr);
}
}
private void handleSliceUnpinned(final Uri sliceUri) {
- if (Looper.myLooper() == Looper.getMainLooper()) {
+ mCallback = "onSliceUnpinned";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
mSliceProvider.onSliceUnpinned(sliceUri);
- } else {
- final CountDownLatch latch = new CountDownLatch(1);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mSliceProvider.onSliceUnpinned(sliceUri);
- latch.countDown();
- }
- });
- try {
- latch.await();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
+ } finally {
+ mHandler.removeCallbacks(mAnr);
}
}
- private Slice handleBindSlice(final Uri sliceUri, final List<SliceSpec> specs,
+ private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
final String callingPkg) {
// This can be removed once Slice#bindSlice is removed and everyone is using
// SliceManager#bindSlice.
@@ -267,25 +240,7 @@
return createPermissionSlice(getContext(), sliceUri, pkg);
}
}
- if (Looper.myLooper() == Looper.getMainLooper()) {
- return onBindSliceStrict(sliceUri, specs, callingPkg);
- } else {
- final CountDownLatch latch = new CountDownLatch(1);
- final Slice[] output = new Slice[1];
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- output[0] = onBindSliceStrict(sliceUri, specs, callingPkg);
- latch.countDown();
- }
- });
- try {
- latch.await();
- return output[0];
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
+ return onBindSliceStrict(sliceUri, specs);
}
/**
@@ -335,8 +290,10 @@
}
}
- private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> specs, String callingPackage) {
+ private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ mCallback = "onBindSlice";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
try {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
@@ -344,11 +301,10 @@
.build());
SliceProvider.setSpecs(specs);
try {
- mBindingPkg = callingPackage;
return mSliceProvider.onBindSlice(sliceUri);
} finally {
- mBindingPkg = null;
SliceProvider.setSpecs(null);
+ mHandler.removeCallbacks(mAnr);
}
} finally {
StrictMode.setThreadPolicy(oldPolicy);
@@ -359,7 +315,7 @@
* Compat version of {@link Slice#bindSlice}.
*/
public static Slice bindSlice(Context context, Uri uri,
- List<SliceSpec> supportedSpecs) {
+ Set<SliceSpec> supportedSpecs) {
ContentProviderClient provider = context.getContentResolver()
.acquireContentProviderClient(uri);
if (provider == null) {
@@ -387,7 +343,7 @@
}
}
- private static void addSpecs(Bundle extras, List<SliceSpec> supportedSpecs) {
+ private static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
ArrayList<String> types = new ArrayList<>();
ArrayList<Integer> revs = new ArrayList<>();
for (SliceSpec spec : supportedSpecs) {
@@ -398,8 +354,8 @@
extras.putIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS, revs);
}
- private static List<SliceSpec> getSpecs(Bundle extras) {
- ArrayList<SliceSpec> specs = new ArrayList<>();
+ private static Set<SliceSpec> getSpecs(Bundle extras) {
+ ArraySet<SliceSpec> specs = new ArraySet<>();
ArrayList<String> types = extras.getStringArrayList(EXTRA_SUPPORTED_SPECS);
ArrayList<Integer> revs = extras.getIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS);
for (int i = 0; i < types.size(); i++) {
@@ -412,7 +368,7 @@
* Compat version of {@link Slice#bindSlice}.
*/
public static Slice bindSlice(Context context, Intent intent,
- List<SliceSpec> supportedSpecs) {
+ Set<SliceSpec> supportedSpecs) {
ContentResolver resolver = context.getContentResolver();
// Check if the intent has data for the slice uri on it and use that
@@ -459,7 +415,7 @@
* Compat version of {@link android.app.slice.SliceManager#pinSlice}.
*/
public static void pinSlice(Context context, Uri uri,
- List<SliceSpec> supportedSpecs) {
+ Set<SliceSpec> supportedSpecs) {
ContentProviderClient provider = context.getContentResolver()
.acquireContentProviderClient(uri);
if (provider == null) {
@@ -483,7 +439,7 @@
* Compat version of {@link android.app.slice.SliceManager#unpinSlice}.
*/
public static void unpinSlice(Context context, Uri uri,
- List<SliceSpec> supportedSpecs) {
+ Set<SliceSpec> supportedSpecs) {
ContentProviderClient provider = context.getContentResolver()
.acquireContentProviderClient(uri);
if (provider == null) {
@@ -506,7 +462,7 @@
/**
* Compat version of {@link android.app.slice.SliceManager#getPinnedSpecs(Uri)}.
*/
- public static List<SliceSpec> getPinnedSpecs(Context context, Uri uri) {
+ public static Set<SliceSpec> getPinnedSpecs(Context context, Uri uri) {
ContentProviderClient provider = context.getContentResolver()
.acquireContentProviderClient(uri);
if (provider == null) {
@@ -578,4 +534,12 @@
return null;
}
}
+
+ private final Runnable mAnr = new Runnable() {
+ @Override
+ public void run() {
+ Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+ Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+ }
+ };
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index e6765f6..65505f7 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -24,13 +24,14 @@
import android.app.slice.SliceSpec;
import android.content.Intent;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.slice.SliceConvert;
import java.util.List;
-import androidx.slice.SliceConvert;
-
/**
* @hide
*/
@@ -55,7 +56,7 @@
@Override
public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedVersions) {
- androidx.slice.SliceProvider.setSpecs(wrap(supportedVersions));
+ androidx.slice.SliceProvider.setSpecs(new ArraySet<>(wrap(supportedVersions)));
try {
return SliceConvert.unwrap(mSliceProvider.onBindSlice(sliceUri));
} finally {
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index f9c305c..c176d3e 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -69,11 +69,11 @@
public final class SliceLiveData {
ctor public SliceLiveData();
- method public static android.arch.lifecycle.LiveData<androidx.slice.Slice> fromIntent(android.content.Context, android.content.Intent);
- method public static android.arch.lifecycle.LiveData<androidx.slice.Slice> fromUri(android.content.Context, android.net.Uri);
+ method public static androidx.lifecycle.LiveData<androidx.slice.Slice> fromIntent(android.content.Context, android.content.Intent);
+ method public static androidx.lifecycle.LiveData<androidx.slice.Slice> fromUri(android.content.Context, android.net.Uri);
}
- public class SliceView extends android.view.ViewGroup implements android.arch.lifecycle.Observer {
+ public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer {
ctor public SliceView(android.content.Context);
ctor public SliceView(android.content.Context, android.util.AttributeSet);
ctor public SliceView(android.content.Context, android.util.AttributeSet, int);
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java b/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
index aa60b38..2ea8071 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
@@ -35,7 +35,7 @@
import android.text.style.ForegroundColorSpan;
import androidx.slice.Slice;
-import androidx.slice.builders.GridBuilder;
+import androidx.slice.builders.GridRowBuilder;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.MessagingSliceBuilder;
import androidx.slice.builders.SliceAction;
@@ -117,69 +117,69 @@
"open weather app"), Icon.createWithResource(getContext(), R.drawable.weather_1),
"Weather is happening!");
ListBuilder b = new ListBuilder(getContext(), sliceUri);
- GridBuilder gb = new GridBuilder(b);
+ GridRowBuilder gb = new GridRowBuilder(b);
gb.setPrimaryAction(primaryAction);
- gb.addCell(new GridBuilder.CellBuilder(gb)
+ gb.addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.weather_1),
SMALL_IMAGE)
.addText("MON")
.addTitleText("69\u00B0"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.weather_2),
SMALL_IMAGE)
.addText("TUE")
.addTitleText("71\u00B0"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.weather_3),
SMALL_IMAGE)
.addText("WED")
.addTitleText("76\u00B0"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.weather_4),
SMALL_IMAGE)
.addText("THU")
.addTitleText("72\u00B0"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.weather_1),
SMALL_IMAGE)
.addText("FRI")
.addTitleText("68\u00B0"));
- return b.addGrid(gb).build();
+ return b.addGridRow(gb).build();
}
private Slice createGallery(Uri sliceUri) {
ListBuilder b = new ListBuilder(getContext(), sliceUri);
- GridBuilder gb = new GridBuilder(b);
+ GridRowBuilder gb = new GridRowBuilder(b);
PendingIntent pi = getBroadcastIntent(ACTION_TOAST, "see more of your gallery");
gb.addSeeMoreAction(pi);
- gb.addCell(new GridBuilder.CellBuilder(gb)
+ gb.addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_1),
LARGE_IMAGE))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_2),
LARGE_IMAGE))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_3),
LARGE_IMAGE))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_4),
LARGE_IMAGE))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_2),
LARGE_IMAGE))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_3),
LARGE_IMAGE))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.slices_4),
LARGE_IMAGE));
- return b.addGrid(gb).build();
+ return b.addGridRow(gb).build();
}
private Slice createSubSlice(Uri sliceUri, boolean customSeeMore) {
ListBuilder b = new ListBuilder(getContext(), sliceUri);
- GridBuilder gb = new GridBuilder(b);
- GridBuilder.CellBuilder cb = new GridBuilder.CellBuilder(gb);
+ GridRowBuilder gb = new GridRowBuilder(b);
+ GridRowBuilder.CellBuilder cb = new GridRowBuilder.CellBuilder(gb);
PendingIntent pi = getBroadcastIntent(ACTION_TOAST, "See cats you follow");
if (customSeeMore) {
cb.addImage(Icon.createWithResource(getContext(), R.drawable.ic_right_caret),
@@ -190,31 +190,31 @@
} else {
gb.addSeeMoreAction(pi);
}
- gb.addCell(new GridBuilder.CellBuilder(gb)
+ gb.addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_1),
SMALL_IMAGE)
.addTitleText("Oreo"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_2),
SMALL_IMAGE)
.addTitleText("Silver"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_3),
SMALL_IMAGE)
.addTitleText("Drake"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_5),
SMALL_IMAGE)
.addTitleText("Olive"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_4),
SMALL_IMAGE)
.addTitleText("Lady Marmalade"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.cat_6),
SMALL_IMAGE)
.addTitleText("Grapefruit"));
- return b.addGrid(gb).build();
+ return b.addGridRow(gb).build();
}
private Slice createContact(Uri sliceUri) {
@@ -223,29 +223,29 @@
SliceAction primaryAction = new SliceAction(getBroadcastIntent(ACTION_TOAST,
"See contact info"), Icon.createWithResource(getContext(),
R.drawable.mady), SMALL_IMAGE, "Mady");
- GridBuilder gb = new GridBuilder(b);
+ GridRowBuilder gb = new GridRowBuilder(b);
return b.setColor(0xff3949ab)
.addRow(rb
.setTitle("Mady Pitza")
.setSubtitle("Frequently contacted contact")
.addEndItem(primaryAction))
- .addGrid(gb
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addGridRow(gb
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.ic_call),
ICON_IMAGE)
.addText("Call")
.setContentIntent(getBroadcastIntent(ACTION_TOAST, "call")))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.ic_text),
ICON_IMAGE)
.addText("Text")
.setContentIntent(getBroadcastIntent(ACTION_TOAST, "text")))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.ic_video),
ICON_IMAGE)
.setContentIntent(getBroadcastIntent(ACTION_TOAST, "video"))
.addText("Video"))
- .addCell(new GridBuilder.CellBuilder(gb)
+ .addCell(new GridRowBuilder.CellBuilder(gb)
.addImage(Icon.createWithResource(getContext(), R.drawable.ic_email),
ICON_IMAGE)
.addText("Email")
@@ -423,16 +423,16 @@
private Slice createReservationSlice(Uri sliceUri) {
ListBuilder lb = new ListBuilder(getContext(), sliceUri);
- GridBuilder gb1 = new GridBuilder(lb);
- gb1.addCell(new GridBuilder.CellBuilder(gb1)
+ GridRowBuilder gb1 = new GridRowBuilder(lb);
+ gb1.addCell(new GridRowBuilder.CellBuilder(gb1)
.addImage(Icon.createWithResource(getContext(), R.drawable.reservation),
LARGE_IMAGE)
.setContentDescription("Image of your reservation in Seattle"));
- GridBuilder gb2 = new GridBuilder(lb);
- gb2.addCell(new GridBuilder.CellBuilder(gb2)
+ GridRowBuilder gb2 = new GridRowBuilder(lb);
+ gb2.addCell(new GridRowBuilder.CellBuilder(gb2)
.addTitleText("Check In")
.addText("12:00 PM, Feb 1"))
- .addCell(new GridBuilder.CellBuilder(gb2)
+ .addCell(new GridRowBuilder.CellBuilder(gb2)
.addTitleText("Check Out")
.addText("11:00 AM, Feb 19"));
return lb.setColor(0xffFF5252)
@@ -446,8 +446,8 @@
.addAction(new SliceAction(getBroadcastIntent(ACTION_TOAST, "contact host"),
Icon.createWithResource(getContext(), R.drawable.ic_text),
"Contact host"))
- .addGrid(gb1)
- .addGrid(gb2)
+ .addGridRow(gb1)
+ .addGridRow(gb2)
.build();
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 2f470ab..a97acd0 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -19,12 +19,13 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.os.BuildCompat;
-import java.util.List;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -125,7 +126,7 @@
* @see SliceSpec
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public abstract @NonNull List<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
+ public abstract @NonNull Set<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
/**
* Turns a slice Uri into slice content.
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
index ee039c9..e68223b 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
@@ -24,11 +24,12 @@
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import android.util.ArrayMap;
import android.util.Pair;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
import java.util.concurrent.Executor;
/**
@@ -58,13 +59,11 @@
@Override
public void registerSliceCallback(@NonNull Uri uri, @NonNull Executor executor,
@NonNull SliceCallback callback) {
- pinSlice(uri);
getListener(uri, callback, new SliceListenerImpl(uri, executor, callback)).startListening();
}
@Override
public void unregisterSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback) {
- unpinSlice(uri);
SliceListenerImpl impl = mListenerLookup.remove(new Pair<>(uri, callback));
if (impl != null) impl.stopListening();
}
@@ -84,6 +83,7 @@
private Uri mUri;
private final Executor mExecutor;
private final SliceCallback mCallback;
+ private boolean mPinned;
SliceListenerImpl(Uri uri, Executor executor, SliceCallback callback) {
mUri = uri;
@@ -93,15 +93,32 @@
void startListening() {
mContext.getContentResolver().registerContentObserver(mUri, true, mObserver);
+ tryPin();
+ }
+
+ private void tryPin() {
+ if (!mPinned) {
+ try {
+ pinSlice(mUri);
+ mPinned = true;
+ } catch (SecurityException e) {
+ // No permission currently.
+ }
+ }
}
void stopListening() {
mContext.getContentResolver().unregisterContentObserver(mObserver);
+ if (mPinned) {
+ unpinSlice(mUri);
+ mPinned = false;
+ }
}
private final Runnable mUpdateSlice = new Runnable() {
@Override
public void run() {
+ tryPin();
final Slice s = Slice.bindSlice(mContext, mUri, SUPPORTED_SPECS);
mExecutor.execute(new Runnable() {
@Override
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
index 0b9e67e..8abacd1 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -21,15 +21,15 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-
-import java.util.List;
-
import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.widget.SliceLiveData;
+import java.util.Set;
+
/**
* @hide
@@ -52,7 +52,7 @@
}
@Override
- public @NonNull List<SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
+ public @NonNull Set<SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
return SliceProviderCompat.getPinnedSpecs(mContext, uri);
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index 6b857f4..040baf6 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -24,12 +24,15 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* @hide
@@ -54,7 +57,7 @@
SliceManagerWrapper(Context context, android.app.slice.SliceManager manager) {
super(context);
mManager = manager;
- mSpecs = unwrap(SUPPORTED_SPECS);
+ mSpecs = new ArrayList<>(unwrap(SUPPORTED_SPECS));
}
@Override
@@ -68,7 +71,7 @@
}
@Override
- public @NonNull List<androidx.slice.SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
+ public @NonNull Set<androidx.slice.SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
return SliceConvert.wrap(mManager.getPinnedSpecs(uri));
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/ActionContent.java b/slices/view/src/main/java/androidx/slice/widget/ActionContent.java
index aedd068..877492d 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ActionContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ActionContent.java
@@ -77,6 +77,8 @@
SUBTYPE_PRIORITY);
mPriority = priority != null ? priority.getInt() : -1;
return true;
+ } else if (FORMAT_ACTION.equals(slice.getFormat())) {
+ mActionItem = slice;
}
return false;
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowContent.java b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
index b6ca48a..e08ceb0 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -37,9 +37,6 @@
import android.text.TextUtils;
import android.util.Log;
-import java.util.ArrayList;
-import java.util.List;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -47,6 +44,9 @@
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Extracts information required to present content in a row format from a slice.
* @hide
@@ -91,6 +91,11 @@
mPrimaryAction = SliceQuery.find(rowSlice, FORMAT_SLICE, hints,
new String[] { HINT_ACTIONS, HINT_KEY_WORDS } /* nonHints */);
+ if (mPrimaryAction == null && FORMAT_ACTION.equals(rowSlice.getFormat())
+ && rowSlice.getSlice().getItems().size() == 1) {
+ mPrimaryAction = rowSlice;
+ }
+
mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION);
// Filter anything not viable for displaying in a row
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index 13e6128..18b0d54 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -51,9 +51,6 @@
import android.widget.TextView;
import android.widget.ToggleButton;
-import java.util.ArrayList;
-import java.util.List;
-
import androidx.annotation.ColorInt;
import androidx.annotation.RestrictTo;
import androidx.slice.Slice;
@@ -61,6 +58,9 @@
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Row item is in small template format and can be used to construct list items for use
* with {@link LargeTemplateView}.
@@ -305,11 +305,18 @@
}
private void addRange(final SliceItem range) {
- final ProgressBar progressBar;
- if (FORMAT_ACTION.equals(range.getFormat())) {
- // An input range is displayed as a seek bar
- progressBar = mSeekBar;
- mSeekBar.setVisibility(View.VISIBLE);
+ final boolean isSeekBar = FORMAT_ACTION.equals(range.getFormat());
+ final ProgressBar progressBar = isSeekBar ? mSeekBar : mProgressBar;
+ SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
+ if (max != null) {
+ progressBar.setMax(max.getInt());
+ }
+ SliceItem progress = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
+ if (progress != null) {
+ progressBar.setProgress(progress.getInt());
+ }
+ progressBar.setVisibility(View.VISIBLE);
+ if (isSeekBar) {
SliceItem thumb = SliceQuery.find(range, FORMAT_IMAGE);
if (thumb != null) {
mSeekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
@@ -331,18 +338,6 @@
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
- } else {
- // A range is displayed as a progress bar.
- progressBar = mProgressBar;
- mProgressBar.setVisibility(View.VISIBLE);
- }
- SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
- if (max != null) {
- progressBar.setMax(max.getInt());
- }
- SliceItem progress = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
- if (progress != null) {
- progressBar.setProgress(progress.getInt());
}
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java b/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
index da6fb2c..aa73ad5 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
@@ -17,22 +17,23 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-import android.arch.lifecycle.LiveData;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
+
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-
-import java.util.Arrays;
-import java.util.List;
-
+import androidx.collection.ArraySet;
+import androidx.lifecycle.LiveData;
import androidx.slice.Slice;
import androidx.slice.SliceManager;
import androidx.slice.SliceSpec;
import androidx.slice.SliceSpecs;
+import java.util.Arrays;
+import java.util.Set;
+
/**
* Class with factory methods for creating LiveData that observes slices.
*
@@ -45,8 +46,20 @@
* @hide
*/
@RestrictTo(LIBRARY)
- public static final List<SliceSpec> SUPPORTED_SPECS = Arrays.asList(SliceSpecs.BASIC,
- SliceSpecs.LIST);
+ public static final SliceSpec OLD_BASIC = new SliceSpec("androidx.app.slice.BASIC", 1);
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final SliceSpec OLD_LIST = new SliceSpec("androidx.app.slice.LIST", 1);
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final Set<SliceSpec> SUPPORTED_SPECS = new ArraySet<>(
+ Arrays.asList(SliceSpecs.BASIC, SliceSpecs.LIST, OLD_BASIC, OLD_LIST));
/**
* Produces an {@link LiveData} that tracks a Slice for a given Uri. To use
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceView.java b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
index 6ef3103..9fb977e 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
@@ -20,27 +20,27 @@
import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_INT;
-import android.arch.lifecycle.Observer;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import java.util.List;
-
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.lifecycle.Observer;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceUtils;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
+import java.util.List;
+
/**
* A view for displaying a {@link Slice} which is a piece of app content and actions. SliceView is
* able to present slice content in a templated format outside of the associated app. The way this
diff --git a/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 17128ea..ab69d56 100644
--- a/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -41,7 +41,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
-import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
@@ -197,18 +196,6 @@
final ArrayList<DisableLayerRunnable> mPostedRunnables = new ArrayList<>();
- static final SlidingPanelLayoutImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 17) {
- IMPL = new SlidingPanelLayoutImplJBMR1();
- } else if (Build.VERSION.SDK_INT >= 16) {
- IMPL = new SlidingPanelLayoutImplJB();
- } else {
- IMPL = new SlidingPanelLayoutImplBase();
- }
- }
-
/**
* Listener for monitoring events about sliding panes.
*/
@@ -1020,8 +1007,56 @@
return result;
}
+ private Method mGetDisplayList;
+ private Field mRecreateDisplayList;
+ private boolean mDisplayListReflectionLoaded;
+
void invalidateChildRegion(View v) {
- IMPL.invalidateChildRegion(this, v);
+ if (Build.VERSION.SDK_INT >= 17) {
+ ViewCompat.setLayerPaint(v, ((LayoutParams) v.getLayoutParams()).dimPaint);
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= 16) {
+ // Private API hacks! Nasty! Bad!
+ //
+ // In Jellybean, some optimizations in the hardware UI renderer
+ // prevent a changed Paint on a View using a hardware layer from having
+ // the intended effect. This twiddles some internal bits on the view to force
+ // it to recreate the display list.
+ if (!mDisplayListReflectionLoaded) {
+ try {
+ mGetDisplayList = View.class.getDeclaredMethod("getDisplayList",
+ (Class[]) null);
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.",
+ e);
+ }
+ try {
+ mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList");
+ mRecreateDisplayList.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.",
+ e);
+ }
+ mDisplayListReflectionLoaded = true;
+ }
+ if (mGetDisplayList == null || mRecreateDisplayList == null) {
+ // Slow path. REALLY slow path. Let's hope we don't get here.
+ v.invalidate();
+ return;
+ }
+
+ try {
+ mRecreateDisplayList.setBoolean(v, true);
+ mGetDisplayList.invoke(v, (Object[]) null);
+ } catch (Exception e) {
+ Log.e(TAG, "Error refreshing display list state", e);
+ }
+ }
+
+ ViewCompat.postInvalidateOnAnimation(this, v.getLeft(), v.getTop(), v.getRight(),
+ v.getBottom());
}
/**
@@ -1475,71 +1510,6 @@
};
}
- interface SlidingPanelLayoutImpl {
- void invalidateChildRegion(SlidingPaneLayout parent, View child);
- }
-
- static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl {
- @Override
- public void invalidateChildRegion(SlidingPaneLayout parent, View child) {
- ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(),
- child.getRight(), child.getBottom());
- }
- }
-
- @RequiresApi(16)
- static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase {
- /*
- * Private API hacks! Nasty! Bad!
- *
- * In Jellybean, some optimizations in the hardware UI renderer
- * prevent a changed Paint on a View using a hardware layer from having
- * the intended effect. This twiddles some internal bits on the view to force
- * it to recreate the display list.
- */
- private Method mGetDisplayList;
- private Field mRecreateDisplayList;
-
- SlidingPanelLayoutImplJB() {
- try {
- mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null);
- } catch (NoSuchMethodException e) {
- Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e);
- }
- try {
- mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList");
- mRecreateDisplayList.setAccessible(true);
- } catch (NoSuchFieldException e) {
- Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e);
- }
- }
-
- @Override
- public void invalidateChildRegion(SlidingPaneLayout parent, View child) {
- if (mGetDisplayList != null && mRecreateDisplayList != null) {
- try {
- mRecreateDisplayList.setBoolean(child, true);
- mGetDisplayList.invoke(child, (Object[]) null);
- } catch (Exception e) {
- Log.e(TAG, "Error refreshing display list state", e);
- }
- } else {
- // Slow path. REALLY slow path. Let's hope we don't get here.
- child.invalidate();
- return;
- }
- super.invalidateChildRegion(parent, child);
- }
- }
-
- @RequiresApi(17)
- static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase {
- @Override
- public void invalidateChildRegion(SlidingPaneLayout parent, View child) {
- ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint);
- }
- }
-
class AccessibilityDelegate extends AccessibilityDelegateCompat {
private final Rect mTmpRect = new Rect();
diff --git a/transition/src/main/java/androidx/transition/AnimatorUtils.java b/transition/src/main/java/androidx/transition/AnimatorUtils.java
index 511528b..452bc66 100644
--- a/transition/src/main/java/androidx/transition/AnimatorUtils.java
+++ b/transition/src/main/java/androidx/transition/AnimatorUtils.java
@@ -22,29 +22,59 @@
import androidx.annotation.NonNull;
+import java.util.ArrayList;
+
class AnimatorUtils {
- private static final AnimatorUtilsImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 19) {
- IMPL = new AnimatorUtilsApi19();
- } else {
- IMPL = new AnimatorUtilsApi14();
- }
- }
-
static void addPauseListener(@NonNull Animator animator,
@NonNull AnimatorListenerAdapter listener) {
- IMPL.addPauseListener(animator, listener);
+ if (Build.VERSION.SDK_INT >= 19) {
+ animator.addPauseListener(listener);
+ }
}
static void pause(@NonNull Animator animator) {
- IMPL.pause(animator);
+ if (Build.VERSION.SDK_INT >= 19) {
+ animator.pause();
+ } else {
+ final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
+ if (listeners != null) {
+ for (int i = 0, size = listeners.size(); i < size; i++) {
+ final Animator.AnimatorListener listener = listeners.get(i);
+ if (listener instanceof AnimatorPauseListenerCompat) {
+ ((AnimatorPauseListenerCompat) listener).onAnimationPause(animator);
+ }
+ }
+ }
+ }
}
static void resume(@NonNull Animator animator) {
- IMPL.resume(animator);
+ if (Build.VERSION.SDK_INT >= 19) {
+ animator.resume();
+ } else {
+ final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
+ if (listeners != null) {
+ for (int i = 0, size = listeners.size(); i < size; i++) {
+ final Animator.AnimatorListener listener = listeners.get(i);
+ if (listener instanceof AnimatorPauseListenerCompat) {
+ ((AnimatorPauseListenerCompat) listener).onAnimationResume(animator);
+ }
+ }
+ }
+ }
}
+ /**
+ * Listeners can implement this interface in addition to the platform AnimatorPauseListener to
+ * make them compatible with API level 18 and below. Animators will not be paused or resumed,
+ * but the callbacks here are invoked.
+ */
+ interface AnimatorPauseListenerCompat {
+
+ void onAnimationPause(Animator animation);
+
+ void onAnimationResume(Animator animation);
+
+ }
}
diff --git a/transition/src/main/java/androidx/transition/AnimatorUtilsApi14.java b/transition/src/main/java/androidx/transition/AnimatorUtilsApi14.java
deleted file mode 100644
index df2c680..0000000
--- a/transition/src/main/java/androidx/transition/AnimatorUtilsApi14.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import java.util.ArrayList;
-
-@RequiresApi(14)
-class AnimatorUtilsApi14 implements AnimatorUtilsImpl {
-
- @Override
- public void addPauseListener(@NonNull Animator animator,
- @NonNull AnimatorListenerAdapter listener) {
- // Do nothing
- }
-
- @Override
- public void pause(@NonNull Animator animator) {
- final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
- if (listeners != null) {
- for (int i = 0, size = listeners.size(); i < size; i++) {
- final Animator.AnimatorListener listener = listeners.get(i);
- if (listener instanceof AnimatorPauseListenerCompat) {
- ((AnimatorPauseListenerCompat) listener).onAnimationPause(animator);
- }
- }
- }
- }
-
- @Override
- public void resume(@NonNull Animator animator) {
- final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
- if (listeners != null) {
- for (int i = 0, size = listeners.size(); i < size; i++) {
- final Animator.AnimatorListener listener = listeners.get(i);
- if (listener instanceof AnimatorPauseListenerCompat) {
- ((AnimatorPauseListenerCompat) listener).onAnimationResume(animator);
- }
- }
- }
- }
-
- /**
- * Listeners can implement this interface in addition to the platform AnimatorPauseListener to
- * make them compatible with API level 18 and below. Animators will not be paused or resumed,
- * but the callbacks here are invoked.
- */
- interface AnimatorPauseListenerCompat {
-
- void onAnimationPause(Animator animation);
-
- void onAnimationResume(Animator animation);
-
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/AnimatorUtilsApi19.java b/transition/src/main/java/androidx/transition/AnimatorUtilsApi19.java
deleted file mode 100644
index 8bab629..0000000
--- a/transition/src/main/java/androidx/transition/AnimatorUtilsApi19.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 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 androidx.transition;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(19)
-class AnimatorUtilsApi19 implements AnimatorUtilsImpl {
-
- @Override
- public void addPauseListener(@NonNull Animator animator,
- @NonNull AnimatorListenerAdapter listener) {
- animator.addPauseListener(listener);
- }
-
- @Override
- public void pause(@NonNull Animator animator) {
- animator.pause();
- }
-
- @Override
- public void resume(@NonNull Animator animator) {
- animator.resume();
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/AnimatorUtilsImpl.java b/transition/src/main/java/androidx/transition/AnimatorUtilsImpl.java
deleted file mode 100644
index 5f61cea..0000000
--- a/transition/src/main/java/androidx/transition/AnimatorUtilsImpl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-
-import androidx.annotation.NonNull;
-
-interface AnimatorUtilsImpl {
-
- void addPauseListener(@NonNull Animator animator, @NonNull AnimatorListenerAdapter listener);
-
- void pause(@NonNull Animator animator);
-
- void resume(@NonNull Animator animator);
-
-}
diff --git a/transition/src/main/java/androidx/transition/GhostViewApi14.java b/transition/src/main/java/androidx/transition/GhostViewApi14.java
index f989e88..fa577d9 100644
--- a/transition/src/main/java/androidx/transition/GhostViewApi14.java
+++ b/transition/src/main/java/androidx/transition/GhostViewApi14.java
@@ -26,7 +26,6 @@
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
/**
@@ -40,58 +39,51 @@
* that, this view is sized as large as the parent FrameLayout (except padding) while the platform
* version becomes as large as the target view.
*/
-@RequiresApi(14)
@SuppressLint("ViewConstructor")
class GhostViewApi14 extends View implements GhostViewImpl {
- static class Creator implements GhostViewImpl.Creator {
-
- @Override
- public GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
- GhostViewApi14 ghostView = getGhostView(view);
- if (ghostView == null) {
- FrameLayout frameLayout = findFrameLayout(viewGroup);
- if (frameLayout == null) {
- return null;
- }
- ghostView = new GhostViewApi14(view);
- frameLayout.addView(ghostView);
+ static GhostViewImpl addGhost(View view, ViewGroup viewGroup) {
+ GhostViewApi14 ghostView = getGhostView(view);
+ if (ghostView == null) {
+ FrameLayout frameLayout = findFrameLayout(viewGroup);
+ if (frameLayout == null) {
+ return null;
}
- ghostView.mReferences++;
- return ghostView;
+ ghostView = new GhostViewApi14(view);
+ frameLayout.addView(ghostView);
}
+ ghostView.mReferences++;
+ return ghostView;
+ }
- @Override
- public void removeGhost(View view) {
- GhostViewApi14 ghostView = getGhostView(view);
- if (ghostView != null) {
- ghostView.mReferences--;
- if (ghostView.mReferences <= 0) {
- ViewParent parent = ghostView.getParent();
- if (parent instanceof ViewGroup) {
- ViewGroup group = (ViewGroup) parent;
- group.endViewTransition(ghostView);
- group.removeView(ghostView);
- }
+ static void removeGhost(View view) {
+ GhostViewApi14 ghostView = getGhostView(view);
+ if (ghostView != null) {
+ ghostView.mReferences--;
+ if (ghostView.mReferences <= 0) {
+ ViewParent parent = ghostView.getParent();
+ if (parent instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) parent;
+ group.endViewTransition(ghostView);
+ group.removeView(ghostView);
}
}
}
+ }
- /**
- * Find the closest FrameLayout in the ascendant hierarchy from the specified {@code
- * viewGroup}.
- */
- private static FrameLayout findFrameLayout(ViewGroup viewGroup) {
- while (!(viewGroup instanceof FrameLayout)) {
- ViewParent parent = viewGroup.getParent();
- if (!(parent instanceof ViewGroup)) {
- return null;
- }
- viewGroup = (ViewGroup) parent;
+ /**
+ * Find the closest FrameLayout in the ascendant hierarchy from the specified {@code
+ * viewGroup}.
+ */
+ private static FrameLayout findFrameLayout(ViewGroup viewGroup) {
+ while (!(viewGroup instanceof FrameLayout)) {
+ ViewParent parent = viewGroup.getParent();
+ if (!(parent instanceof ViewGroup)) {
+ return null;
}
- return (FrameLayout) viewGroup;
+ viewGroup = (ViewGroup) parent;
}
-
+ return (FrameLayout) viewGroup;
}
/** The target view */
diff --git a/transition/src/main/java/androidx/transition/GhostViewApi21.java b/transition/src/main/java/androidx/transition/GhostViewApi21.java
index 70897e9..4cf5ae8 100644
--- a/transition/src/main/java/androidx/transition/GhostViewApi21.java
+++ b/transition/src/main/java/androidx/transition/GhostViewApi21.java
@@ -39,38 +39,32 @@
private static Method sRemoveGhostMethod;
private static boolean sRemoveGhostMethodFetched;
- static class Creator implements GhostViewImpl.Creator {
-
- @Override
- public GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
- fetchAddGhostMethod();
- if (sAddGhostMethod != null) {
- try {
- return new GhostViewApi21(
- (View) sAddGhostMethod.invoke(null, view, viewGroup, matrix));
- } catch (IllegalAccessException e) {
- // Do nothing
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e.getCause());
- }
- }
- return null;
- }
-
- @Override
- public void removeGhost(View view) {
- fetchRemoveGhostMethod();
- if (sRemoveGhostMethod != null) {
- try {
- sRemoveGhostMethod.invoke(null, view);
- } catch (IllegalAccessException e) {
- // Do nothing
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e.getCause());
- }
+ static GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
+ fetchAddGhostMethod();
+ if (sAddGhostMethod != null) {
+ try {
+ return new GhostViewApi21(
+ (View) sAddGhostMethod.invoke(null, view, viewGroup, matrix));
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
}
}
+ return null;
+ }
+ static void removeGhost(View view) {
+ fetchRemoveGhostMethod();
+ if (sRemoveGhostMethod != null) {
+ try {
+ sRemoveGhostMethod.invoke(null, view);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
}
/** A handle to the platform android.view.GhostView. */
diff --git a/transition/src/main/java/androidx/transition/GhostViewImpl.java b/transition/src/main/java/androidx/transition/GhostViewImpl.java
index 42a4593..5a4d6cf 100644
--- a/transition/src/main/java/androidx/transition/GhostViewImpl.java
+++ b/transition/src/main/java/androidx/transition/GhostViewImpl.java
@@ -16,23 +16,11 @@
package androidx.transition;
-import android.graphics.Matrix;
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
interface GhostViewImpl {
- interface Creator {
-
- GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix);
-
- void removeGhost(View view);
-
- }
-
void setVisibility(int visibility);
/**
diff --git a/transition/src/main/java/androidx/transition/GhostViewUtils.java b/transition/src/main/java/androidx/transition/GhostViewUtils.java
index 8e8a752..447f728 100644
--- a/transition/src/main/java/androidx/transition/GhostViewUtils.java
+++ b/transition/src/main/java/androidx/transition/GhostViewUtils.java
@@ -23,22 +23,19 @@
class GhostViewUtils {
- private static final GhostViewImpl.Creator CREATOR;
-
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- CREATOR = new GhostViewApi21.Creator();
- } else {
- CREATOR = new GhostViewApi14.Creator();
- }
- }
-
static GhostViewImpl addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
- return CREATOR.addGhost(view, viewGroup, matrix);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return GhostViewApi21.addGhost(view, viewGroup, matrix);
+ }
+ return GhostViewApi14.addGhost(view, viewGroup);
}
static void removeGhost(View view) {
- CREATOR.removeGhost(view);
+ if (Build.VERSION.SDK_INT >= 21) {
+ GhostViewApi21.removeGhost(view);
+ } else {
+ GhostViewApi14.removeGhost(view);
+ }
}
}
diff --git a/transition/src/main/java/androidx/transition/ImageViewUtils.java b/transition/src/main/java/androidx/transition/ImageViewUtils.java
index 8bdec01..a7c947c 100644
--- a/transition/src/main/java/androidx/transition/ImageViewUtils.java
+++ b/transition/src/main/java/androidx/transition/ImageViewUtils.java
@@ -17,43 +17,92 @@
package androidx.transition;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.graphics.Matrix;
import android.os.Build;
+import android.util.Log;
import android.widget.ImageView;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
class ImageViewUtils {
+ private static final String TAG = "ImageViewUtils";
- private static final ImageViewUtilsImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new ImageViewUtilsApi21();
- } else {
- IMPL = new ImageViewUtilsApi14();
- }
- }
+ private static Method sAnimateTransformMethod;
+ private static boolean sAnimateTransformMethodFetched;
/**
* Starts animating the transformation of the image view. This has to be called before calling
* {@link #animateTransform(ImageView, Matrix)}.
*/
static void startAnimateTransform(ImageView view) {
- IMPL.startAnimateTransform(view);
+ if (Build.VERSION.SDK_INT < 21) {
+ final ImageView.ScaleType scaleType = view.getScaleType();
+ view.setTag(R.id.save_scale_type, scaleType);
+ if (scaleType == ImageView.ScaleType.MATRIX) {
+ view.setTag(R.id.save_image_matrix, view.getImageMatrix());
+ } else {
+ view.setScaleType(ImageView.ScaleType.MATRIX);
+ }
+ view.setImageMatrix(MatrixUtils.IDENTITY_MATRIX);
+ }
}
/**
* Sets the matrix to animate the content of the image view.
*/
static void animateTransform(ImageView view, Matrix matrix) {
- IMPL.animateTransform(view, matrix);
+ if (Build.VERSION.SDK_INT < 21) {
+ view.setImageMatrix(matrix);
+ } else {
+ fetchAnimateTransformMethod();
+ if (sAnimateTransformMethod != null) {
+ try {
+ sAnimateTransformMethod.invoke(view, matrix);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ }
+ }
+ }
+
+ private static void fetchAnimateTransformMethod() {
+ if (!sAnimateTransformMethodFetched) {
+ try {
+ sAnimateTransformMethod = ImageView.class.getDeclaredMethod("animateTransform",
+ Matrix.class);
+ sAnimateTransformMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve animateTransform method", e);
+ }
+ sAnimateTransformMethodFetched = true;
+ }
}
/**
* Reserves that the caller will stop calling {@link #animateTransform(ImageView, Matrix)} when
* the specified animator ends.
*/
- static void reserveEndAnimateTransform(ImageView view, Animator animator) {
- IMPL.reserveEndAnimateTransform(view, animator);
+ static void reserveEndAnimateTransform(final ImageView view, Animator animator) {
+ if (Build.VERSION.SDK_INT < 21) {
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final ImageView.ScaleType scaleType = (ImageView.ScaleType)
+ view.getTag(R.id.save_scale_type);
+ view.setScaleType(scaleType);
+ view.setTag(R.id.save_scale_type, null);
+ if (scaleType == ImageView.ScaleType.MATRIX) {
+ view.setImageMatrix((Matrix) view.getTag(R.id.save_image_matrix));
+ view.setTag(R.id.save_image_matrix, null);
+ }
+ animation.removeListener(this);
+ }
+ });
+ }
}
}
diff --git a/transition/src/main/java/androidx/transition/ImageViewUtilsApi14.java b/transition/src/main/java/androidx/transition/ImageViewUtilsApi14.java
deleted file mode 100644
index 4e8f8c1..0000000
--- a/transition/src/main/java/androidx/transition/ImageViewUtilsApi14.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.graphics.Matrix;
-import android.widget.ImageView;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-class ImageViewUtilsApi14 implements ImageViewUtilsImpl {
-
- @Override
- public void startAnimateTransform(ImageView view) {
- final ImageView.ScaleType scaleType = view.getScaleType();
- view.setTag(R.id.save_scale_type, scaleType);
- if (scaleType == ImageView.ScaleType.MATRIX) {
- view.setTag(R.id.save_image_matrix, view.getImageMatrix());
- } else {
- view.setScaleType(ImageView.ScaleType.MATRIX);
- }
- view.setImageMatrix(MatrixUtils.IDENTITY_MATRIX);
- }
-
- @Override
- public void animateTransform(ImageView view, Matrix matrix) {
- view.setImageMatrix(matrix);
- }
-
- @Override
- public void reserveEndAnimateTransform(final ImageView view, Animator animator) {
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- final ImageView.ScaleType scaleType = (ImageView.ScaleType)
- view.getTag(R.id.save_scale_type);
- view.setScaleType(scaleType);
- view.setTag(R.id.save_scale_type, null);
- if (scaleType == ImageView.ScaleType.MATRIX) {
- view.setImageMatrix((Matrix) view.getTag(R.id.save_image_matrix));
- view.setTag(R.id.save_image_matrix, null);
- }
- animation.removeListener(this);
- }
- });
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/ImageViewUtilsApi21.java b/transition/src/main/java/androidx/transition/ImageViewUtilsApi21.java
deleted file mode 100644
index c5b7f56..0000000
--- a/transition/src/main/java/androidx/transition/ImageViewUtilsApi21.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.Animator;
-import android.graphics.Matrix;
-import android.util.Log;
-import android.widget.ImageView;
-
-import androidx.annotation.RequiresApi;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-@RequiresApi(21)
-class ImageViewUtilsApi21 implements ImageViewUtilsImpl {
-
- private static final String TAG = "ImageViewUtilsApi21";
-
- private static Method sAnimateTransformMethod;
- private static boolean sAnimateTransformMethodFetched;
-
- @Override
- public void startAnimateTransform(ImageView view) {
- // Do nothing
- }
-
- @Override
- public void animateTransform(ImageView view, Matrix matrix) {
- fetchAnimateTransformMethod();
- if (sAnimateTransformMethod != null) {
- try {
- sAnimateTransformMethod.invoke(view, matrix);
- } catch (IllegalAccessException e) {
- // Do nothing
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e.getCause());
- }
- }
- }
-
- @Override
- public void reserveEndAnimateTransform(ImageView view, Animator animator) {
- // Do nothing
- }
-
- private void fetchAnimateTransformMethod() {
- if (!sAnimateTransformMethodFetched) {
- try {
- sAnimateTransformMethod = ImageView.class.getDeclaredMethod("animateTransform",
- Matrix.class);
- sAnimateTransformMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.i(TAG, "Failed to retrieve animateTransform method", e);
- }
- sAnimateTransformMethodFetched = true;
- }
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/ImageViewUtilsImpl.java b/transition/src/main/java/androidx/transition/ImageViewUtilsImpl.java
deleted file mode 100644
index 5006de3..0000000
--- a/transition/src/main/java/androidx/transition/ImageViewUtilsImpl.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.Animator;
-import android.graphics.Matrix;
-import android.widget.ImageView;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-interface ImageViewUtilsImpl {
-
- void startAnimateTransform(ImageView view);
-
- void animateTransform(ImageView view, Matrix matrix);
-
- void reserveEndAnimateTransform(ImageView view, Animator animator);
-
-}
diff --git a/transition/src/main/java/androidx/transition/ObjectAnimatorUtils.java b/transition/src/main/java/androidx/transition/ObjectAnimatorUtils.java
index 281b2ef..a7fb97d 100644
--- a/transition/src/main/java/androidx/transition/ObjectAnimatorUtils.java
+++ b/transition/src/main/java/androidx/transition/ObjectAnimatorUtils.java
@@ -24,18 +24,11 @@
class ObjectAnimatorUtils {
- private static final ObjectAnimatorUtilsImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new ObjectAnimatorUtilsApi21();
- } else {
- IMPL = new ObjectAnimatorUtilsApi14();
- }
- }
-
static <T> ObjectAnimator ofPointF(T target, Property<T, PointF> property, Path path) {
- return IMPL.ofPointF(target, property, path);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return ObjectAnimator.ofObject(target, property, null, path);
+ }
+ return ObjectAnimator.ofFloat(target, new PathProperty<>(property, path), 0f, 1f);
}
}
diff --git a/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsApi14.java b/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsApi14.java
deleted file mode 100644
index 92cc5df..0000000
--- a/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsApi14.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.ObjectAnimator;
-import android.graphics.Path;
-import android.graphics.PointF;
-import android.util.Property;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-class ObjectAnimatorUtilsApi14 implements ObjectAnimatorUtilsImpl {
-
- @Override
- public <T> ObjectAnimator ofPointF(T target, Property<T, PointF> property, Path path) {
- return ObjectAnimator.ofFloat(target, new PathProperty<>(property, path), 0f, 1f);
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsApi21.java b/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsApi21.java
deleted file mode 100644
index 09b07bf..0000000
--- a/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsApi21.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.ObjectAnimator;
-import android.graphics.Path;
-import android.graphics.PointF;
-import android.util.Property;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(21)
-class ObjectAnimatorUtilsApi21 implements ObjectAnimatorUtilsImpl {
-
- @Override
- public <T> ObjectAnimator ofPointF(T target, Property<T, PointF> property, Path path) {
- return ObjectAnimator.ofObject(target, property, null, path);
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsImpl.java b/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsImpl.java
deleted file mode 100644
index c3af025..0000000
--- a/transition/src/main/java/androidx/transition/ObjectAnimatorUtilsImpl.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.ObjectAnimator;
-import android.graphics.Path;
-import android.graphics.PointF;
-import android.util.Property;
-
-interface ObjectAnimatorUtilsImpl {
-
- <T> ObjectAnimator ofPointF(T target, Property<T, PointF> property, Path path);
-
-}
diff --git a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtils.java b/transition/src/main/java/androidx/transition/PropertyValuesHolderUtils.java
index 819ec0d..2ea657c 100644
--- a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtils.java
+++ b/transition/src/main/java/androidx/transition/PropertyValuesHolderUtils.java
@@ -24,16 +24,6 @@
class PropertyValuesHolderUtils {
- private static final PropertyValuesHolderUtilsImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- IMPL = new PropertyValuesHolderUtilsApi21();
- } else {
- IMPL = new PropertyValuesHolderUtilsApi14();
- }
- }
-
/**
* Constructs and returns a PropertyValuesHolder with a given property and
* a Path along which the values should be animated. This variant supports a
@@ -45,7 +35,10 @@
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
*/
static PropertyValuesHolder ofPointF(Property<?, PointF> property, Path path) {
- return IMPL.ofPointF(property, path);
+ if (Build.VERSION.SDK_INT >= 21) {
+ return PropertyValuesHolder.ofObject(property, null, path);
+ }
+ return PropertyValuesHolder.ofFloat(new PathProperty<>(property, path), 0f, 1f);
}
}
diff --git a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsApi14.java b/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsApi14.java
deleted file mode 100644
index 047500d..0000000
--- a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsApi14.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.PropertyValuesHolder;
-import android.graphics.Path;
-import android.graphics.PointF;
-import android.util.Property;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-class PropertyValuesHolderUtilsApi14 implements PropertyValuesHolderUtilsImpl {
-
- @Override
- public PropertyValuesHolder ofPointF(Property<?, PointF> property, Path path) {
- return PropertyValuesHolder.ofFloat(new PathProperty<>(property, path), 0f, 1f);
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsApi21.java b/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsApi21.java
deleted file mode 100644
index c5a1170..0000000
--- a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsApi21.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.PropertyValuesHolder;
-import android.graphics.Path;
-import android.graphics.PointF;
-import android.util.Property;
-
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(21)
-class PropertyValuesHolderUtilsApi21 implements PropertyValuesHolderUtilsImpl {
-
- @Override
- public PropertyValuesHolder ofPointF(Property<?, PointF> property, Path path) {
- return PropertyValuesHolder.ofObject(property, null, path);
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsImpl.java b/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsImpl.java
deleted file mode 100644
index b5bd6ea..0000000
--- a/transition/src/main/java/androidx/transition/PropertyValuesHolderUtilsImpl.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 androidx.transition;
-
-import android.animation.PropertyValuesHolder;
-import android.graphics.Path;
-import android.graphics.PointF;
-import android.util.Property;
-
-interface PropertyValuesHolderUtilsImpl {
-
- PropertyValuesHolder ofPointF(Property<?, PointF> property, Path path);
-
-}
diff --git a/transition/src/main/java/androidx/transition/ViewGroupUtils.java b/transition/src/main/java/androidx/transition/ViewGroupUtils.java
index 0077005..55ab3ff 100644
--- a/transition/src/main/java/androidx/transition/ViewGroupUtils.java
+++ b/transition/src/main/java/androidx/transition/ViewGroupUtils.java
@@ -26,28 +26,25 @@
*/
class ViewGroupUtils {
- private static final ViewGroupUtilsImpl IMPL;
-
- static {
- if (Build.VERSION.SDK_INT >= 18) {
- IMPL = new ViewGroupUtilsApi18();
- } else {
- IMPL = new ViewGroupUtilsApi14();
- }
- }
-
/**
* Backward-compatible {@link ViewGroup#getOverlay()}.
*/
static ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group) {
- return IMPL.getOverlay(group);
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new ViewGroupOverlayApi18(group);
+ }
+ return ViewGroupOverlayApi14.createFrom(group);
}
/**
* Provides access to the hidden ViewGroup#suppressLayout method.
*/
static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
- IMPL.suppressLayout(group, suppress);
+ if (Build.VERSION.SDK_INT >= 18) {
+ ViewGroupUtilsApi18.suppressLayout(group, suppress);
+ } else {
+ ViewGroupUtilsApi14.suppressLayout(group, suppress);
+ }
}
}
diff --git a/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java b/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
index 3843062..70af497 100644
--- a/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
+++ b/transition/src/main/java/androidx/transition/ViewGroupUtilsApi14.java
@@ -21,14 +21,12 @@
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-@RequiresApi(14)
-class ViewGroupUtilsApi14 implements ViewGroupUtilsImpl {
+class ViewGroupUtilsApi14 {
private static final String TAG = "ViewGroupUtilsApi14";
@@ -42,13 +40,7 @@
private static Method sCancelMethod;
private static boolean sCancelMethodFetched;
- @Override
- public ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group) {
- return ViewGroupOverlayApi14.createFrom(group);
- }
-
- @Override
- public void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
+ static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
// Prepare the dummy LayoutTransition
if (sEmptyLayoutTransition == null) {
sEmptyLayoutTransition = new LayoutTransition() {
diff --git a/transition/src/main/java/androidx/transition/ViewGroupUtilsApi18.java b/transition/src/main/java/androidx/transition/ViewGroupUtilsApi18.java
index 94d6dc6..bd0938a 100644
--- a/transition/src/main/java/androidx/transition/ViewGroupUtilsApi18.java
+++ b/transition/src/main/java/androidx/transition/ViewGroupUtilsApi18.java
@@ -26,20 +26,14 @@
import java.lang.reflect.Method;
@RequiresApi(18)
-class ViewGroupUtilsApi18 extends ViewGroupUtilsApi14 {
+class ViewGroupUtilsApi18 {
private static final String TAG = "ViewUtilsApi18";
private static Method sSuppressLayoutMethod;
private static boolean sSuppressLayoutMethodFetched;
- @Override
- public ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group) {
- return new ViewGroupOverlayApi18(group);
- }
-
- @Override
- public void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
+ static void suppressLayout(@NonNull ViewGroup group, boolean suppress) {
fetchSuppressLayoutMethod();
if (sSuppressLayoutMethod != null) {
try {
@@ -52,7 +46,7 @@
}
}
- private void fetchSuppressLayoutMethod() {
+ private static void fetchSuppressLayoutMethod() {
if (!sSuppressLayoutMethodFetched) {
try {
sSuppressLayoutMethod = ViewGroup.class.getDeclaredMethod("suppressLayout",
diff --git a/transition/src/main/java/androidx/transition/ViewGroupUtilsImpl.java b/transition/src/main/java/androidx/transition/ViewGroupUtilsImpl.java
deleted file mode 100644
index 2de6fde..0000000
--- a/transition/src/main/java/androidx/transition/ViewGroupUtilsImpl.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2016 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 androidx.transition;
-
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-interface ViewGroupUtilsImpl {
-
- ViewGroupOverlayImpl getOverlay(@NonNull ViewGroup group);
-
- void suppressLayout(@NonNull ViewGroup group, boolean suppress);
-
-}
diff --git a/transition/src/main/java/androidx/transition/ViewUtils.java b/transition/src/main/java/androidx/transition/ViewUtils.java
index 8200f7c..89eaadd 100644
--- a/transition/src/main/java/androidx/transition/ViewUtils.java
+++ b/transition/src/main/java/androidx/transition/ViewUtils.java
@@ -34,7 +34,7 @@
*/
class ViewUtils {
- private static final ViewUtilsImpl IMPL;
+ private static final ViewUtilsBase IMPL;
private static final String TAG = "ViewUtils";
private static Field sViewFlagsField;
@@ -48,10 +48,8 @@
IMPL = new ViewUtilsApi21();
} else if (Build.VERSION.SDK_INT >= 19) {
IMPL = new ViewUtilsApi19();
- } else if (Build.VERSION.SDK_INT >= 18) {
- IMPL = new ViewUtilsApi18();
} else {
- IMPL = new ViewUtilsApi14();
+ IMPL = new ViewUtilsBase();
}
}
@@ -92,14 +90,20 @@
* Backward-compatible {@link View#getOverlay()}.
*/
static ViewOverlayImpl getOverlay(@NonNull View view) {
- return IMPL.getOverlay(view);
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new ViewOverlayApi18(view);
+ }
+ return ViewOverlayApi14.createFrom(view);
}
/**
* Backward-compatible {@link View#getWindowId()}.
*/
static WindowIdImpl getWindowId(@NonNull View view) {
- return IMPL.getWindowId(view);
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new WindowIdApi18(view);
+ }
+ return new WindowIdApi14(view.getWindowToken());
}
static void setTransitionAlpha(@NonNull View view, float alpha) {
diff --git a/transition/src/main/java/androidx/transition/ViewUtilsApi14.java b/transition/src/main/java/androidx/transition/ViewUtilsApi14.java
deleted file mode 100644
index f52786e..0000000
--- a/transition/src/main/java/androidx/transition/ViewUtilsApi14.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2016 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 androidx.transition;
-
-import android.graphics.Matrix;
-import android.view.View;
-import android.view.ViewParent;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-class ViewUtilsApi14 implements ViewUtilsImpl {
-
- private float[] mMatrixValues;
-
- @Override
- public ViewOverlayImpl getOverlay(@NonNull View view) {
- return ViewOverlayApi14.createFrom(view);
- }
-
- @Override
- public WindowIdImpl getWindowId(@NonNull View view) {
- return new WindowIdApi14(view.getWindowToken());
- }
-
- @Override
- public void setTransitionAlpha(@NonNull View view, float alpha) {
- Float savedAlpha = (Float) view.getTag(R.id.save_non_transition_alpha);
- if (savedAlpha != null) {
- view.setAlpha(savedAlpha * alpha);
- } else {
- view.setAlpha(alpha);
- }
- }
-
- @Override
- public float getTransitionAlpha(@NonNull View view) {
- Float savedAlpha = (Float) view.getTag(R.id.save_non_transition_alpha);
- if (savedAlpha != null) {
- return view.getAlpha() / savedAlpha;
- } else {
- return view.getAlpha();
- }
- }
-
- @Override
- public void saveNonTransitionAlpha(@NonNull View view) {
- if (view.getTag(R.id.save_non_transition_alpha) == null) {
- view.setTag(R.id.save_non_transition_alpha, view.getAlpha());
- }
- }
-
- @Override
- public void clearNonTransitionAlpha(@NonNull View view) {
- // We don't clear the saved value when the view is hidden; that's the situation we are
- // saving this value for.
- if (view.getVisibility() == View.VISIBLE) {
- view.setTag(R.id.save_non_transition_alpha, null);
- }
- }
-
- @Override
- public void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix) {
- final ViewParent parent = view.getParent();
- if (parent instanceof View) {
- final View vp = (View) parent;
- transformMatrixToGlobal(vp, matrix);
- matrix.preTranslate(-vp.getScrollX(), -vp.getScrollY());
- }
- matrix.preTranslate(view.getLeft(), view.getTop());
- final Matrix vm = view.getMatrix();
- if (!vm.isIdentity()) {
- matrix.preConcat(vm);
- }
- }
-
- @Override
- public void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix) {
- final ViewParent parent = view.getParent();
- if (parent instanceof View) {
- final View vp = (View) parent;
- transformMatrixToLocal(vp, matrix);
- matrix.postTranslate(vp.getScrollX(), vp.getScrollY());
- }
- matrix.postTranslate(view.getLeft(), view.getTop());
- final Matrix vm = view.getMatrix();
- if (!vm.isIdentity()) {
- final Matrix inverted = new Matrix();
- if (vm.invert(inverted)) {
- matrix.postConcat(inverted);
- }
- }
- }
-
- @Override
- public void setAnimationMatrix(@NonNull View view, Matrix matrix) {
- if (matrix == null || matrix.isIdentity()) {
- view.setPivotX(view.getWidth() / 2);
- view.setPivotY(view.getHeight() / 2);
- view.setTranslationX(0);
- view.setTranslationY(0);
- view.setScaleX(1);
- view.setScaleY(1);
- view.setRotation(0);
- } else {
- float[] values = mMatrixValues;
- if (values == null) {
- mMatrixValues = values = new float[9];
- }
- matrix.getValues(values);
- final float sin = values[Matrix.MSKEW_Y];
- final float cos = (float) Math.sqrt(1 - sin * sin)
- * (values[Matrix.MSCALE_X] < 0 ? -1 : 1);
- final float rotation = (float) Math.toDegrees(Math.atan2(sin, cos));
- final float scaleX = values[Matrix.MSCALE_X] / cos;
- final float scaleY = values[Matrix.MSCALE_Y] / cos;
- final float dx = values[Matrix.MTRANS_X];
- final float dy = values[Matrix.MTRANS_Y];
- view.setPivotX(0);
- view.setPivotY(0);
- view.setTranslationX(dx);
- view.setTranslationY(dy);
- view.setRotation(rotation);
- view.setScaleX(scaleX);
- view.setScaleY(scaleY);
- }
- }
-
- @Override
- public void setLeftTopRightBottom(View v, int left, int top, int right, int bottom) {
- v.setLeft(left);
- v.setTop(top);
- v.setRight(right);
- v.setBottom(bottom);
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/ViewUtilsApi18.java b/transition/src/main/java/androidx/transition/ViewUtilsApi18.java
deleted file mode 100644
index 5ef3b62..0000000
--- a/transition/src/main/java/androidx/transition/ViewUtilsApi18.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 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 androidx.transition;
-
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(18)
-class ViewUtilsApi18 extends ViewUtilsApi14 {
-
- @Override
- public ViewOverlayImpl getOverlay(@NonNull View view) {
- return new ViewOverlayApi18(view);
- }
-
- @Override
- public WindowIdImpl getWindowId(@NonNull View view) {
- return new WindowIdApi18(view);
- }
-
-}
diff --git a/transition/src/main/java/androidx/transition/ViewUtilsApi19.java b/transition/src/main/java/androidx/transition/ViewUtilsApi19.java
index a2f627b..ff60431 100644
--- a/transition/src/main/java/androidx/transition/ViewUtilsApi19.java
+++ b/transition/src/main/java/androidx/transition/ViewUtilsApi19.java
@@ -26,7 +26,7 @@
import java.lang.reflect.Method;
@RequiresApi(19)
-class ViewUtilsApi19 extends ViewUtilsApi18 {
+class ViewUtilsApi19 extends ViewUtilsBase {
private static final String TAG = "ViewUtilsApi19";
diff --git a/transition/src/main/java/androidx/transition/ViewUtilsBase.java b/transition/src/main/java/androidx/transition/ViewUtilsBase.java
new file mode 100644
index 0000000..c3dad8f
--- /dev/null
+++ b/transition/src/main/java/androidx/transition/ViewUtilsBase.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 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 androidx.transition;
+
+import android.graphics.Matrix;
+import android.view.View;
+import android.view.ViewParent;
+
+import androidx.annotation.NonNull;
+
+class ViewUtilsBase {
+
+ private float[] mMatrixValues;
+
+ public void setTransitionAlpha(@NonNull View view, float alpha) {
+ Float savedAlpha = (Float) view.getTag(R.id.save_non_transition_alpha);
+ if (savedAlpha != null) {
+ view.setAlpha(savedAlpha * alpha);
+ } else {
+ view.setAlpha(alpha);
+ }
+ }
+
+ public float getTransitionAlpha(@NonNull View view) {
+ Float savedAlpha = (Float) view.getTag(R.id.save_non_transition_alpha);
+ if (savedAlpha != null) {
+ return view.getAlpha() / savedAlpha;
+ } else {
+ return view.getAlpha();
+ }
+ }
+
+ public void saveNonTransitionAlpha(@NonNull View view) {
+ if (view.getTag(R.id.save_non_transition_alpha) == null) {
+ view.setTag(R.id.save_non_transition_alpha, view.getAlpha());
+ }
+ }
+
+ public void clearNonTransitionAlpha(@NonNull View view) {
+ // We don't clear the saved value when the view is hidden; that's the situation we are
+ // saving this value for.
+ if (view.getVisibility() == View.VISIBLE) {
+ view.setTag(R.id.save_non_transition_alpha, null);
+ }
+ }
+
+ public void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix) {
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ transformMatrixToGlobal(vp, matrix);
+ matrix.preTranslate(-vp.getScrollX(), -vp.getScrollY());
+ }
+ matrix.preTranslate(view.getLeft(), view.getTop());
+ final Matrix vm = view.getMatrix();
+ if (!vm.isIdentity()) {
+ matrix.preConcat(vm);
+ }
+ }
+
+ public void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix) {
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ final View vp = (View) parent;
+ transformMatrixToLocal(vp, matrix);
+ matrix.postTranslate(vp.getScrollX(), vp.getScrollY());
+ }
+ matrix.postTranslate(view.getLeft(), view.getTop());
+ final Matrix vm = view.getMatrix();
+ if (!vm.isIdentity()) {
+ final Matrix inverted = new Matrix();
+ if (vm.invert(inverted)) {
+ matrix.postConcat(inverted);
+ }
+ }
+ }
+
+ public void setAnimationMatrix(@NonNull View view, Matrix matrix) {
+ if (matrix == null || matrix.isIdentity()) {
+ view.setPivotX(view.getWidth() / 2);
+ view.setPivotY(view.getHeight() / 2);
+ view.setTranslationX(0);
+ view.setTranslationY(0);
+ view.setScaleX(1);
+ view.setScaleY(1);
+ view.setRotation(0);
+ } else {
+ float[] values = mMatrixValues;
+ if (values == null) {
+ mMatrixValues = values = new float[9];
+ }
+ matrix.getValues(values);
+ final float sin = values[Matrix.MSKEW_Y];
+ final float cos = (float) Math.sqrt(1 - sin * sin)
+ * (values[Matrix.MSCALE_X] < 0 ? -1 : 1);
+ final float rotation = (float) Math.toDegrees(Math.atan2(sin, cos));
+ final float scaleX = values[Matrix.MSCALE_X] / cos;
+ final float scaleY = values[Matrix.MSCALE_Y] / cos;
+ final float dx = values[Matrix.MTRANS_X];
+ final float dy = values[Matrix.MTRANS_Y];
+ view.setPivotX(0);
+ view.setPivotY(0);
+ view.setTranslationX(dx);
+ view.setTranslationY(dy);
+ view.setRotation(rotation);
+ view.setScaleX(scaleX);
+ view.setScaleY(scaleY);
+ }
+ }
+
+ public void setLeftTopRightBottom(View v, int left, int top, int right, int bottom) {
+ v.setLeft(left);
+ v.setTop(top);
+ v.setRight(right);
+ v.setBottom(bottom);
+ }
+
+}
diff --git a/transition/src/main/java/androidx/transition/ViewUtilsImpl.java b/transition/src/main/java/androidx/transition/ViewUtilsImpl.java
deleted file mode 100644
index 50b4fb4..0000000
--- a/transition/src/main/java/androidx/transition/ViewUtilsImpl.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 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 androidx.transition;
-
-import android.graphics.Matrix;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-@RequiresApi(14)
-interface ViewUtilsImpl {
-
- ViewOverlayImpl getOverlay(@NonNull View view);
-
- WindowIdImpl getWindowId(@NonNull View view);
-
- void setTransitionAlpha(@NonNull View view, float alpha);
-
- float getTransitionAlpha(@NonNull View view);
-
- void saveNonTransitionAlpha(@NonNull View view);
-
- void clearNonTransitionAlpha(@NonNull View view);
-
- void transformMatrixToGlobal(@NonNull View view, @NonNull Matrix matrix);
-
- void transformMatrixToLocal(@NonNull View view, @NonNull Matrix matrix);
-
- void setAnimationMatrix(@NonNull View view, Matrix matrix);
-
- void setLeftTopRightBottom(View v, int left, int top, int right, int bottom);
-
-}
diff --git a/transition/src/main/java/androidx/transition/Visibility.java b/transition/src/main/java/androidx/transition/Visibility.java
index 3726bf7..f26a547 100644
--- a/transition/src/main/java/androidx/transition/Visibility.java
+++ b/transition/src/main/java/androidx/transition/Visibility.java
@@ -469,7 +469,7 @@
}
private static class DisappearListener extends AnimatorListenerAdapter
- implements TransitionListener, AnimatorUtilsApi14.AnimatorPauseListenerCompat {
+ implements TransitionListener, AnimatorUtils.AnimatorPauseListenerCompat {
private final View mView;
private final int mFinalVisibility;
diff --git a/v7/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java b/v7/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java
index 6502aab..c079f44 100644
--- a/v7/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java
+++ b/v7/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeTestCase.java
@@ -114,8 +114,8 @@
// Set MODE_NIGHT_AUTO so that we will change to night mode automatically
final NightModeActivity newActivity =
setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
- final AppCompatDelegateImplV14 newDelegate =
- (AppCompatDelegateImplV14) newActivity.getDelegate();
+ final AppCompatDelegateImplBase newDelegate =
+ (AppCompatDelegateImplBase) newActivity.getDelegate();
// Update the fake twilight manager to be in night and trigger a fake 'time' change
mActivityTestRule.runOnUiThread(new Runnable() {
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index d93ca95..b66dec3 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -200,7 +200,7 @@
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else {
- return new AppCompatDelegateImplV14(context, window, callback);
+ return new AppCompatDelegateImplBase(context, window, callback);
}
}
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplBase.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplBase.java
index 6be3804..eb4c63e 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplBase.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplBase.java
@@ -16,40 +16,110 @@
package androidx.appcompat.app;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.Window.FEATURE_OPTIONS_PANEL;
+
import android.app.Activity;
+import android.app.Dialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
+import android.widget.PopupWindow;
+import android.widget.TextView;
-import androidx.annotation.RequiresApi;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.appcompat.R;
+import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.view.ActionMode;
+import androidx.appcompat.view.ContextThemeWrapper;
+import androidx.appcompat.view.StandaloneActionMode;
+import androidx.appcompat.view.SupportActionModeWrapper;
import androidx.appcompat.view.SupportMenuInflater;
import androidx.appcompat.view.WindowCallbackWrapper;
+import androidx.appcompat.view.menu.ListMenuPresenter;
import androidx.appcompat.view.menu.MenuBuilder;
+import androidx.appcompat.view.menu.MenuPresenter;
+import androidx.appcompat.view.menu.MenuView;
+import androidx.appcompat.widget.ActionBarContextView;
+import androidx.appcompat.widget.AppCompatDrawableManager;
+import androidx.appcompat.widget.ContentFrameLayout;
+import androidx.appcompat.widget.DecorContentParent;
+import androidx.appcompat.widget.FitWindowsViewGroup;
import androidx.appcompat.widget.TintTypedArray;
+import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.widget.VectorEnabledTintResources;
+import androidx.appcompat.widget.ViewStubCompat;
+import androidx.appcompat.widget.ViewUtils;
+import androidx.core.app.NavUtils;
+import androidx.core.view.LayoutInflaterCompat;
+import androidx.core.view.OnApplyWindowInsetsListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.ViewPropertyAnimatorCompat;
+import androidx.core.view.ViewPropertyAnimatorListenerAdapter;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.widget.PopupWindowCompat;
-@RequiresApi(14)
-abstract class AppCompatDelegateImplBase extends AppCompatDelegate {
+import org.xmlpull.v1.XmlPullParser;
- static final boolean DEBUG = false;
+class AppCompatDelegateImplBase extends AppCompatDelegate
+ implements MenuBuilder.Callback, LayoutInflater.Factory2 {
+
+ private static final boolean DEBUG = false;
+ private static final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
+ private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode";
+
+ private static final int[] sWindowBackgroundStyleable = {android.R.attr.windowBackground};
private static boolean sInstalledExceptionHandler;
- private static final boolean SHOULD_INSTALL_EXCEPTION_HANDLER = Build.VERSION.SDK_INT < 21;
static final String EXCEPTION_HANDLER_MESSAGE_SUFFIX= ". If the resource you are"
+ " trying to use is a vector resource, you may be referencing it in an unsupported"
+ " way. See AppCompatDelegate.setCompatVectorFromResourcesEnabled() for more info.";
static {
- if (SHOULD_INSTALL_EXCEPTION_HANDLER && !sInstalledExceptionHandler) {
+ if (IS_PRE_LOLLIPOP && !sInstalledExceptionHandler) {
final Thread.UncaughtExceptionHandler defHandler
= Thread.getDefaultUncaughtExceptionHandler();
@@ -82,8 +152,6 @@
}
}
- private static final int[] sWindowBackgroundStyleable = {android.R.attr.windowBackground};
-
final Context mContext;
final Window mWindow;
final Window.Callback mOriginalWindowCallback;
@@ -93,6 +161,30 @@
ActionBar mActionBar;
MenuInflater mMenuInflater;
+ private CharSequence mTitle;
+
+ private DecorContentParent mDecorContentParent;
+ private ActionMenuPresenterCallback mActionMenuPresenterCallback;
+ private PanelMenuPresenterCallback mPanelMenuPresenterCallback;
+
+ ActionMode mActionMode;
+ ActionBarContextView mActionModeView;
+ PopupWindow mActionModePopup;
+ Runnable mShowActionModePopup;
+ ViewPropertyAnimatorCompat mFadeAnim = null;
+
+ private boolean mHandleNativeActionModes = true; // defaults to true
+
+ // true if we have installed a window sub-decor layout.
+ private boolean mSubDecorInstalled;
+ private ViewGroup mSubDecor;
+
+ private TextView mTitleView;
+ private View mStatusGuard;
+
+ // Used to keep track of Progress Bar Window features
+ private boolean mFeatureProgress, mFeatureIndeterminateProgress;
+
// true if this activity has an action bar.
boolean mHasActionBar;
// true if this activity's action bar overlays other activity content.
@@ -104,11 +196,43 @@
// true if this activity has no title
boolean mWindowNoTitle;
- private CharSequence mTitle;
+ // Used for emulating PanelFeatureState
+ private boolean mClosingActionMenu;
+ private PanelFeatureState[] mPanels;
+ private PanelFeatureState mPreparedPanel;
- private boolean mIsStarted;
+ private boolean mLongPressBackDown;
+
private boolean mIsDestroyed;
- private boolean mEatKeyUpEvent;
+
+ @NightMode
+ private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED;
+ private boolean mApplyDayNightCalled;
+
+ private AutoNightModeManager mAutoNightModeManager;
+
+ boolean mInvalidatePanelMenuPosted;
+ int mInvalidatePanelMenuFeatures;
+ private final Runnable mInvalidatePanelMenuRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if ((mInvalidatePanelMenuFeatures & 1 << FEATURE_OPTIONS_PANEL) != 0) {
+ doInvalidatePanelMenu(FEATURE_OPTIONS_PANEL);
+ }
+ if ((mInvalidatePanelMenuFeatures & 1 << FEATURE_SUPPORT_ACTION_BAR) != 0) {
+ doInvalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
+ }
+ mInvalidatePanelMenuPosted = false;
+ mInvalidatePanelMenuFeatures = 0;
+ }
+ };
+
+ private boolean mEnableDefaultActionBarUp;
+
+ private Rect mTempRect1;
+ private Rect mTempRect2;
+
+ private AppCompatViewInflater mAppCompatViewInflater;
AppCompatDelegateImplBase(Context context, Window window, AppCompatCallback callback) {
mContext = context;
@@ -116,7 +240,7 @@
mAppCompatCallback = callback;
mOriginalWindowCallback = mWindow.getCallback();
- if (mOriginalWindowCallback instanceof AppCompatWindowCallbackBase) {
+ if (mOriginalWindowCallback instanceof AppCompatWindowCallbackV14) {
throw new IllegalStateException(
"AppCompat has already installed itself into the Window");
}
@@ -133,10 +257,32 @@
a.recycle();
}
- abstract void initWindowDecorActionBar();
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (mOriginalWindowCallback instanceof Activity) {
+ if (NavUtils.getParentActivityName((Activity) mOriginalWindowCallback) != null) {
+ // Peek at the Action Bar and update it if it already exists
+ ActionBar ab = peekSupportActionBar();
+ if (ab == null) {
+ mEnableDefaultActionBarUp = true;
+ } else {
+ ab.setDefaultDisplayHomeAsUpEnabled(true);
+ }
+ }
+ }
- Window.Callback wrapWindowCallback(Window.Callback callback) {
- return new AppCompatWindowCallbackBase(callback);
+ if (savedInstanceState != null && mLocalNightMode == MODE_NIGHT_UNSPECIFIED) {
+ // If we have a icicle and we haven't had a local night mode set yet, try and read
+ // it from the icicle
+ mLocalNightMode = savedInstanceState.getInt(KEY_LOCAL_NIGHT_MODE,
+ MODE_NIGHT_UNSPECIFIED);
+ }
+ }
+
+ @Override
+ public void onPostCreate(Bundle savedInstanceState) {
+ // Make sure that the sub decor is installed
+ ensureSubDecor();
}
@Override
@@ -151,34 +297,67 @@
return mActionBar;
}
- @Override
- public MenuInflater getMenuInflater() {
- // Make sure that action views can get an appropriate theme.
- if (mMenuInflater == null) {
- initWindowDecorActionBar();
- mMenuInflater = new SupportMenuInflater(
- mActionBar != null ? mActionBar.getThemedContext() : mContext);
+ Window.Callback wrapWindowCallback(Window.Callback callback) {
+ return new AppCompatWindowCallbackV14(callback);
+ }
+
+ final Window.Callback getWindowCallback() {
+ return mWindow.getCallback();
+ }
+
+ private void initWindowDecorActionBar() {
+ ensureSubDecor();
+
+ if (!mHasActionBar || mActionBar != null) {
+ return;
}
- return mMenuInflater;
- }
- // Methods used to create and respond to options menu
- abstract void onPanelClosed(int featureId, Menu menu);
-
- abstract boolean onMenuOpened(int featureId, Menu menu);
-
- abstract boolean dispatchKeyEvent(KeyEvent event);
-
- abstract boolean onKeyShortcut(int keyCode, KeyEvent event);
-
- @Override
- public void setLocalNightMode(@NightMode int mode) {
- // no-op
+ if (mOriginalWindowCallback instanceof Activity) {
+ mActionBar = new WindowDecorActionBar((Activity) mOriginalWindowCallback,
+ mOverlayActionBar);
+ } else if (mOriginalWindowCallback instanceof Dialog) {
+ mActionBar = new WindowDecorActionBar((Dialog) mOriginalWindowCallback);
+ }
+ if (mActionBar != null) {
+ mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
+ }
}
@Override
- public final ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
- return new ActionBarDrawableToggleImpl();
+ public void setSupportActionBar(Toolbar toolbar) {
+ if (!(mOriginalWindowCallback instanceof Activity)) {
+ // Only Activities support custom Action Bars
+ return;
+ }
+
+ final ActionBar ab = getSupportActionBar();
+ if (ab instanceof WindowDecorActionBar) {
+ throw new IllegalStateException("This Activity already has an action bar supplied " +
+ "by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set " +
+ "windowActionBar to false in your theme to use a Toolbar instead.");
+ }
+
+ // If we reach here then we're setting a new action bar
+ // First clear out the MenuInflater to make sure that it is valid for the new Action Bar
+ mMenuInflater = null;
+
+ // If we have an action bar currently, destroy it
+ if (ab != null) {
+ ab.onDestroy();
+ }
+
+ if (toolbar != null) {
+ final ToolbarActionBar tbab = new ToolbarActionBar(toolbar,
+ ((Activity) mOriginalWindowCallback).getTitle(), mAppCompatWindowCallback);
+ mActionBar = tbab;
+ mWindow.setCallback(tbab.getWrappedWindowCallback());
+ } else {
+ mActionBar = null;
+ // Re-set the original window callback since we may have already set a Toolbar wrapper
+ mWindow.setCallback(mAppCompatWindowCallback);
+ }
+
+ invalidateOptionsMenu();
}
final Context getActionBarThemedContext() {
@@ -196,107 +375,466 @@
return context;
}
- private class ActionBarDrawableToggleImpl implements ActionBarDrawerToggle.Delegate {
- ActionBarDrawableToggleImpl() {
+ @Override
+ public MenuInflater getMenuInflater() {
+ // Make sure that action views can get an appropriate theme.
+ if (mMenuInflater == null) {
+ initWindowDecorActionBar();
+ mMenuInflater = new SupportMenuInflater(
+ mActionBar != null ? mActionBar.getThemedContext() : mContext);
}
-
- @Override
- public Drawable getThemeUpIndicator() {
- final TintTypedArray a = TintTypedArray.obtainStyledAttributes(
- getActionBarThemedContext(), null, new int[]{ R.attr.homeAsUpIndicator });
- final Drawable result = a.getDrawable(0);
- a.recycle();
- return result;
- }
-
- @Override
- public Context getActionBarThemedContext() {
- return AppCompatDelegateImplBase.this.getActionBarThemedContext();
- }
-
- @Override
- public boolean isNavigationVisible() {
- final ActionBar ab = getSupportActionBar();
- return ab != null && (ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0;
- }
-
- @Override
- public void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.setHomeAsUpIndicator(upDrawable);
- ab.setHomeActionContentDescription(contentDescRes);
- }
- }
-
- @Override
- public void setActionBarDescription(int contentDescRes) {
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.setHomeActionContentDescription(contentDescRes);
- }
- }
+ return mMenuInflater;
}
- abstract ActionMode startSupportActionModeFromWindow(ActionMode.Callback callback);
+ @SuppressWarnings("TypeParameterUnusedInFormals")
+ @Nullable
+ @Override
+ public <T extends View> T findViewById(@IdRes int id) {
+ ensureSubDecor();
+ return (T) mWindow.findViewById(id);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // If this is called before sub-decor is installed, ActionBar will not
+ // be properly initialized.
+ if (mHasActionBar && mSubDecorInstalled) {
+ // Note: The action bar will need to access
+ // view changes from superclass.
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.onConfigurationChanged(newConfig);
+ }
+ }
+
+ // Make sure that the DrawableManager knows about the new config
+ AppCompatDrawableManager.get().onConfigurationChanged(mContext);
+
+ // Re-apply Day/Night to the new configuration
+ applyDayNight();
+ }
@Override
public void onStart() {
- mIsStarted = true;
+ // This will apply day/night if the time has changed, it will also call through to
+ // setupAutoNightModeIfNeeded()
+ applyDayNight();
}
@Override
public void onStop() {
- mIsStarted = false;
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.setShowHideAnimationEnabled(false);
+ }
+
+ // Make sure we clean up any receivers setup for AUTO mode
+ if (mAutoNightModeManager != null) {
+ mAutoNightModeManager.cleanup();
+ }
+ }
+
+ @Override
+ public void onPostResume() {
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.setShowHideAnimationEnabled(true);
+ }
+ }
+
+ @Override
+ public void setContentView(View v) {
+ ensureSubDecor();
+ ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+ contentParent.removeAllViews();
+ contentParent.addView(v);
+ mOriginalWindowCallback.onContentChanged();
+ }
+
+ @Override
+ public void setContentView(int resId) {
+ ensureSubDecor();
+ ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+ contentParent.removeAllViews();
+ LayoutInflater.from(mContext).inflate(resId, contentParent);
+ mOriginalWindowCallback.onContentChanged();
+ }
+
+ @Override
+ public void setContentView(View v, ViewGroup.LayoutParams lp) {
+ ensureSubDecor();
+ ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+ contentParent.removeAllViews();
+ contentParent.addView(v, lp);
+ mOriginalWindowCallback.onContentChanged();
+ }
+
+ @Override
+ public void addContentView(View v, ViewGroup.LayoutParams lp) {
+ ensureSubDecor();
+ ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
+ contentParent.addView(v, lp);
+ mOriginalWindowCallback.onContentChanged();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED) {
+ // If we have a local night mode set, save it
+ outState.putInt(KEY_LOCAL_NIGHT_MODE, mLocalNightMode);
+ }
}
@Override
public void onDestroy() {
+ if (mInvalidatePanelMenuPosted) {
+ mWindow.getDecorView().removeCallbacks(mInvalidatePanelMenuRunnable);
+ }
+
mIsDestroyed = true;
+
+ if (mActionBar != null) {
+ mActionBar.onDestroy();
+ }
+
+ // Make sure we clean up any receivers setup for AUTO mode
+ if (mAutoNightModeManager != null) {
+ mAutoNightModeManager.cleanup();
+ }
+ }
+
+ private void ensureSubDecor() {
+ if (!mSubDecorInstalled) {
+ mSubDecor = createSubDecor();
+
+ // If a title was set before we installed the decor, propagate it now
+ CharSequence title = getTitle();
+ if (!TextUtils.isEmpty(title)) {
+ if (mDecorContentParent != null) {
+ mDecorContentParent.setWindowTitle(title);
+ } else if (peekSupportActionBar() != null) {
+ peekSupportActionBar().setWindowTitle(title);
+ } else if (mTitleView != null) {
+ mTitleView.setText(title);
+ }
+ }
+
+ applyFixedSizeWindow();
+
+ onSubDecorInstalled(mSubDecor);
+
+ mSubDecorInstalled = true;
+
+ // Invalidate if the panel menu hasn't been created before this.
+ // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
+ // being called in the middle of onCreate or similar.
+ // A pending invalidation will typically be resolved before the posted message
+ // would run normally in order to satisfy instance state restoration.
+ PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+ if (!mIsDestroyed && (st == null || st.menu == null)) {
+ invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
+ }
+ }
+ }
+
+ private ViewGroup createSubDecor() {
+ TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
+
+ if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
+ a.recycle();
+ throw new IllegalStateException(
+ "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
+ }
+
+ if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
+ // Don't allow an action bar if there is no title.
+ requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
+ }
+ if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
+ requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
+ }
+ if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
+ requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
+ }
+ mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
+ a.recycle();
+
+ // Now let's make sure that the Window has installed its decor by retrieving it
+ mWindow.getDecorView();
+
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ ViewGroup subDecor = null;
+
+
+ if (!mWindowNoTitle) {
+ if (mIsFloating) {
+ // If we're floating, inflate the dialog title decor
+ subDecor = (ViewGroup) inflater.inflate(
+ R.layout.abc_dialog_title_material, null);
+
+ // Floating windows can never have an action bar, reset the flags
+ mHasActionBar = mOverlayActionBar = false;
+ } else if (mHasActionBar) {
+ /**
+ * This needs some explanation. As we can not use the android:theme attribute
+ * pre-L, we emulate it by manually creating a LayoutInflater using a
+ * ContextThemeWrapper pointing to actionBarTheme.
+ */
+ TypedValue outValue = new TypedValue();
+ mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
+
+ Context themedContext;
+ if (outValue.resourceId != 0) {
+ themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
+ } else {
+ themedContext = mContext;
+ }
+
+ // Now inflate the view using the themed context and set it as the content view
+ subDecor = (ViewGroup) LayoutInflater.from(themedContext)
+ .inflate(R.layout.abc_screen_toolbar, null);
+
+ mDecorContentParent = (DecorContentParent) subDecor
+ .findViewById(R.id.decor_content_parent);
+ mDecorContentParent.setWindowCallback(getWindowCallback());
+
+ /**
+ * Propagate features to DecorContentParent
+ */
+ if (mOverlayActionBar) {
+ mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
+ }
+ if (mFeatureProgress) {
+ mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
+ }
+ if (mFeatureIndeterminateProgress) {
+ mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ }
+ }
+ } else {
+ if (mOverlayActionMode) {
+ subDecor = (ViewGroup) inflater.inflate(
+ R.layout.abc_screen_simple_overlay_action_mode, null);
+ } else {
+ subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
+ }
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ // If we're running on L or above, we can rely on ViewCompat's
+ // setOnApplyWindowInsetsListener
+ ViewCompat.setOnApplyWindowInsetsListener(subDecor,
+ new OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(View v,
+ WindowInsetsCompat insets) {
+ final int top = insets.getSystemWindowInsetTop();
+ final int newTop = updateStatusGuard(top);
+
+ if (top != newTop) {
+ insets = insets.replaceSystemWindowInsets(
+ insets.getSystemWindowInsetLeft(),
+ newTop,
+ insets.getSystemWindowInsetRight(),
+ insets.getSystemWindowInsetBottom());
+ }
+
+ // Now apply the insets on our view
+ return ViewCompat.onApplyWindowInsets(v, insets);
+ }
+ });
+ } else {
+ // Else, we need to use our own FitWindowsViewGroup handling
+ ((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
+ new FitWindowsViewGroup.OnFitSystemWindowsListener() {
+ @Override
+ public void onFitSystemWindows(Rect insets) {
+ insets.top = updateStatusGuard(insets.top);
+ }
+ });
+ }
+ }
+
+ if (subDecor == null) {
+ throw new IllegalArgumentException(
+ "AppCompat does not support the current theme features: { "
+ + "windowActionBar: " + mHasActionBar
+ + ", windowActionBarOverlay: "+ mOverlayActionBar
+ + ", android:windowIsFloating: " + mIsFloating
+ + ", windowActionModeOverlay: " + mOverlayActionMode
+ + ", windowNoTitle: " + mWindowNoTitle
+ + " }");
+ }
+
+ if (mDecorContentParent == null) {
+ mTitleView = (TextView) subDecor.findViewById(R.id.title);
+ }
+
+ // Make the decor optionally fit system windows, like the window's decor
+ ViewUtils.makeOptionalFitsSystemWindows(subDecor);
+
+ final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
+ R.id.action_bar_activity_content);
+
+ final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
+ if (windowContentView != null) {
+ // There might be Views already added to the Window's content view so we need to
+ // migrate them to our content view
+ while (windowContentView.getChildCount() > 0) {
+ final View child = windowContentView.getChildAt(0);
+ windowContentView.removeViewAt(0);
+ contentView.addView(child);
+ }
+
+ // Change our content FrameLayout to use the android.R.id.content id.
+ // Useful for fragments.
+ windowContentView.setId(View.NO_ID);
+ contentView.setId(android.R.id.content);
+
+ // The decorContent may have a foreground drawable set (windowContentOverlay).
+ // Remove this as we handle it ourselves
+ if (windowContentView instanceof FrameLayout) {
+ ((FrameLayout) windowContentView).setForeground(null);
+ }
+ }
+
+ // Now set the Window's content view with the decor
+ mWindow.setContentView(subDecor);
+
+ contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
+ @Override
+ public void onAttachedFromWindow() {}
+
+ @Override
+ public void onDetachedFromWindow() {
+ dismissPopups();
+ }
+ });
+
+ return subDecor;
+ }
+
+ void onSubDecorInstalled(ViewGroup subDecor) {}
+
+ private void applyFixedSizeWindow() {
+ ContentFrameLayout cfl = (ContentFrameLayout) mSubDecor.findViewById(android.R.id.content);
+
+ // This is a bit weird. In the framework, the window sizing attributes control
+ // the decor view's size, meaning that any padding is inset for the min/max widths below.
+ // We don't control measurement at that level, so we need to workaround it by making sure
+ // that the decor view's padding is taken into account.
+ final View windowDecor = mWindow.getDecorView();
+ cfl.setDecorPadding(windowDecor.getPaddingLeft(),
+ windowDecor.getPaddingTop(), windowDecor.getPaddingRight(),
+ windowDecor.getPaddingBottom());
+
+ TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
+ a.getValue(R.styleable.AppCompatTheme_windowMinWidthMajor, cfl.getMinWidthMajor());
+ a.getValue(R.styleable.AppCompatTheme_windowMinWidthMinor, cfl.getMinWidthMinor());
+
+ if (a.hasValue(R.styleable.AppCompatTheme_windowFixedWidthMajor)) {
+ a.getValue(R.styleable.AppCompatTheme_windowFixedWidthMajor,
+ cfl.getFixedWidthMajor());
+ }
+ if (a.hasValue(R.styleable.AppCompatTheme_windowFixedWidthMinor)) {
+ a.getValue(R.styleable.AppCompatTheme_windowFixedWidthMinor,
+ cfl.getFixedWidthMinor());
+ }
+ if (a.hasValue(R.styleable.AppCompatTheme_windowFixedHeightMajor)) {
+ a.getValue(R.styleable.AppCompatTheme_windowFixedHeightMajor,
+ cfl.getFixedHeightMajor());
+ }
+ if (a.hasValue(R.styleable.AppCompatTheme_windowFixedHeightMinor)) {
+ a.getValue(R.styleable.AppCompatTheme_windowFixedHeightMinor,
+ cfl.getFixedHeightMinor());
+ }
+ a.recycle();
+
+ cfl.requestLayout();
}
@Override
- public void setHandleNativeActionModesEnabled(boolean enabled) {
- // no-op pre-v14
+ public boolean requestWindowFeature(int featureId) {
+ featureId = sanitizeWindowFeatureId(featureId);
+
+ if (mWindowNoTitle && featureId == FEATURE_SUPPORT_ACTION_BAR) {
+ return false; // Ignore. No title dominates.
+ }
+ if (mHasActionBar && featureId == Window.FEATURE_NO_TITLE) {
+ // Remove the action bar feature if we have no title. No title dominates.
+ mHasActionBar = false;
+ }
+
+ switch (featureId) {
+ case FEATURE_SUPPORT_ACTION_BAR:
+ throwFeatureRequestIfSubDecorInstalled();
+ mHasActionBar = true;
+ return true;
+ case FEATURE_SUPPORT_ACTION_BAR_OVERLAY:
+ throwFeatureRequestIfSubDecorInstalled();
+ mOverlayActionBar = true;
+ return true;
+ case FEATURE_ACTION_MODE_OVERLAY:
+ throwFeatureRequestIfSubDecorInstalled();
+ mOverlayActionMode = true;
+ return true;
+ case Window.FEATURE_PROGRESS:
+ throwFeatureRequestIfSubDecorInstalled();
+ mFeatureProgress = true;
+ return true;
+ case Window.FEATURE_INDETERMINATE_PROGRESS:
+ throwFeatureRequestIfSubDecorInstalled();
+ mFeatureIndeterminateProgress = true;
+ return true;
+ case Window.FEATURE_NO_TITLE:
+ throwFeatureRequestIfSubDecorInstalled();
+ mWindowNoTitle = true;
+ return true;
+ }
+
+ return mWindow.requestFeature(featureId);
}
@Override
- public boolean isHandleNativeActionModesEnabled() {
- // Always false pre-v14
- return false;
- }
-
- @Override
- public boolean applyDayNight() {
- // no-op on v7
- return false;
- }
-
- final boolean isDestroyed() {
- return mIsDestroyed;
- }
-
- final boolean isStarted() {
- return mIsStarted;
- }
-
- final Window.Callback getWindowCallback() {
- return mWindow.getCallback();
+ public boolean hasWindowFeature(int featureId) {
+ boolean result = false;
+ switch (sanitizeWindowFeatureId(featureId)) {
+ case FEATURE_SUPPORT_ACTION_BAR:
+ result = mHasActionBar;
+ break;
+ case FEATURE_SUPPORT_ACTION_BAR_OVERLAY:
+ result = mOverlayActionBar;
+ break;
+ case FEATURE_ACTION_MODE_OVERLAY:
+ result = mOverlayActionMode;
+ break;
+ case Window.FEATURE_PROGRESS:
+ result = mFeatureProgress;
+ break;
+ case Window.FEATURE_INDETERMINATE_PROGRESS:
+ result = mFeatureIndeterminateProgress;
+ break;
+ case Window.FEATURE_NO_TITLE:
+ result = mWindowNoTitle;
+ break;
+ }
+ return result || mWindow.hasFeature(featureId);
}
@Override
public final void setTitle(CharSequence title) {
mTitle = title;
- onTitleChanged(title);
- }
- @Override
- public void onSaveInstanceState(Bundle outState) {
- // no-op
+ if (mDecorContentParent != null) {
+ mDecorContentParent.setWindowTitle(title);
+ } else if (peekSupportActionBar() != null) {
+ peekSupportActionBar().setWindowTitle(title);
+ } else if (mTitleView != null) {
+ mTitleView.setText(title);
+ }
}
- abstract void onTitleChanged(CharSequence title);
-
final CharSequence getTitle() {
// If the original window callback is an Activity, we'll use its title
if (mOriginalWindowCallback instanceof Activity) {
@@ -306,8 +844,1658 @@
return mTitle;
}
- class AppCompatWindowCallbackBase extends WindowCallbackWrapper {
- AppCompatWindowCallbackBase(Window.Callback callback) {
+ void onPanelClosed(final int featureId) {
+ if (featureId == FEATURE_SUPPORT_ACTION_BAR) {
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.dispatchMenuVisibilityChanged(false);
+ }
+ } else if (featureId == FEATURE_OPTIONS_PANEL) {
+ // Make sure that the options panel is closed. This is mainly used when we're using a
+ // ToolbarActionBar
+ PanelFeatureState st = getPanelState(featureId, true);
+ if (st.isOpen) {
+ closePanel(st, false);
+ }
+ }
+ }
+
+ void onMenuOpened(final int featureId) {
+ if (featureId == FEATURE_SUPPORT_ACTION_BAR) {
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.dispatchMenuVisibilityChanged(true);
+ }
+ }
+ }
+
+ @Override
+ public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+ final Window.Callback cb = getWindowCallback();
+ if (cb != null && !mIsDestroyed) {
+ final PanelFeatureState panel = findMenuPanel(menu.getRootMenu());
+ if (panel != null) {
+ return cb.onMenuItemSelected(panel.featureId, item);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onMenuModeChange(MenuBuilder menu) {
+ reopenMenu(menu, true);
+ }
+
+ @Override
+ public ActionMode startSupportActionMode(@NonNull final ActionMode.Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("ActionMode callback can not be null.");
+ }
+
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+
+ final ActionMode.Callback wrappedCallback = new ActionModeCallbackWrapperV9(callback);
+
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ mActionMode = ab.startActionMode(wrappedCallback);
+ if (mActionMode != null && mAppCompatCallback != null) {
+ mAppCompatCallback.onSupportActionModeStarted(mActionMode);
+ }
+ }
+
+ if (mActionMode == null) {
+ // If the action bar didn't provide an action mode, start the emulated window one
+ mActionMode = startSupportActionModeFromWindow(wrappedCallback);
+ }
+
+ return mActionMode;
+ }
+
+ @Override
+ public void invalidateOptionsMenu() {
+ final ActionBar ab = getSupportActionBar();
+ if (ab != null && ab.invalidateOptionsMenu()) return;
+
+ invalidatePanelMenu(FEATURE_OPTIONS_PANEL);
+ }
+
+ ActionMode startSupportActionModeFromWindow(@NonNull ActionMode.Callback callback) {
+ endOnGoingFadeAnimation();
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+
+ if (!(callback instanceof ActionModeCallbackWrapperV9)) {
+ // If the callback hasn't been wrapped yet, wrap it
+ callback = new ActionModeCallbackWrapperV9(callback);
+ }
+
+ ActionMode mode = null;
+ if (mAppCompatCallback != null && !mIsDestroyed) {
+ try {
+ mode = mAppCompatCallback.onWindowStartingSupportActionMode(callback);
+ } catch (AbstractMethodError ame) {
+ // Older apps might not implement this callback method.
+ }
+ }
+
+ if (mode != null) {
+ mActionMode = mode;
+ } else {
+ if (mActionModeView == null) {
+ if (mIsFloating) {
+ // Use the action bar theme.
+ final TypedValue outValue = new TypedValue();
+ final Resources.Theme baseTheme = mContext.getTheme();
+ baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true);
+
+ final Context actionBarContext;
+ if (outValue.resourceId != 0) {
+ final Resources.Theme actionBarTheme = mContext.getResources().newTheme();
+ actionBarTheme.setTo(baseTheme);
+ actionBarTheme.applyStyle(outValue.resourceId, true);
+
+ actionBarContext = new ContextThemeWrapper(mContext, 0);
+ actionBarContext.getTheme().setTo(actionBarTheme);
+ } else {
+ actionBarContext = mContext;
+ }
+
+ mActionModeView = new ActionBarContextView(actionBarContext);
+ mActionModePopup = new PopupWindow(actionBarContext, null,
+ R.attr.actionModePopupWindowStyle);
+ PopupWindowCompat.setWindowLayoutType(mActionModePopup,
+ WindowManager.LayoutParams.TYPE_APPLICATION);
+ mActionModePopup.setContentView(mActionModeView);
+ mActionModePopup.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
+
+ actionBarContext.getTheme().resolveAttribute(
+ R.attr.actionBarSize, outValue, true);
+ final int height = TypedValue.complexToDimensionPixelSize(outValue.data,
+ actionBarContext.getResources().getDisplayMetrics());
+ mActionModeView.setContentHeight(height);
+ mActionModePopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mShowActionModePopup = new Runnable() {
+ @Override
+ public void run() {
+ mActionModePopup.showAtLocation(
+ mActionModeView,
+ Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0);
+ endOnGoingFadeAnimation();
+
+ if (shouldAnimateActionModeView()) {
+ mActionModeView.setAlpha(0f);
+ mFadeAnim = ViewCompat.animate(mActionModeView).alpha(1f);
+ mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(View view) {
+ mActionModeView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ mActionModeView.setAlpha(1f);
+ mFadeAnim.setListener(null);
+ mFadeAnim = null;
+ }
+ });
+ } else {
+ mActionModeView.setAlpha(1f);
+ mActionModeView.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ } else {
+ ViewStubCompat stub = (ViewStubCompat) mSubDecor
+ .findViewById(R.id.action_mode_bar_stub);
+ if (stub != null) {
+ // Set the layout inflater so that it is inflated with the action bar's context
+ stub.setLayoutInflater(LayoutInflater.from(getActionBarThemedContext()));
+ mActionModeView = (ActionBarContextView) stub.inflate();
+ }
+ }
+ }
+
+ if (mActionModeView != null) {
+ endOnGoingFadeAnimation();
+ mActionModeView.killMode();
+ mode = new StandaloneActionMode(mActionModeView.getContext(), mActionModeView,
+ callback, mActionModePopup == null);
+ if (callback.onCreateActionMode(mode, mode.getMenu())) {
+ mode.invalidate();
+ mActionModeView.initForMode(mode);
+ mActionMode = mode;
+
+ if (shouldAnimateActionModeView()) {
+ mActionModeView.setAlpha(0f);
+ mFadeAnim = ViewCompat.animate(mActionModeView).alpha(1f);
+ mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(View view) {
+ mActionModeView.setVisibility(View.VISIBLE);
+ mActionModeView.sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ if (mActionModeView.getParent() instanceof View) {
+ ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ mActionModeView.setAlpha(1f);
+ mFadeAnim.setListener(null);
+ mFadeAnim = null;
+ }
+ });
+ } else {
+ mActionModeView.setAlpha(1f);
+ mActionModeView.setVisibility(View.VISIBLE);
+ mActionModeView.sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ if (mActionModeView.getParent() instanceof View) {
+ ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
+ }
+ }
+
+ if (mActionModePopup != null) {
+ mWindow.getDecorView().post(mShowActionModePopup);
+ }
+ } else {
+ mActionMode = null;
+ }
+ }
+ }
+ if (mActionMode != null && mAppCompatCallback != null) {
+ mAppCompatCallback.onSupportActionModeStarted(mActionMode);
+ }
+ return mActionMode;
+ }
+
+ final boolean shouldAnimateActionModeView() {
+ // We only to animate the action mode in if the sub decor has already been laid out.
+ // If it hasn't been laid out, it hasn't been drawn to screen yet.
+ return mSubDecorInstalled && mSubDecor != null && ViewCompat.isLaidOut(mSubDecor);
+ }
+
+ @Override
+ public void setHandleNativeActionModesEnabled(boolean enabled) {
+ mHandleNativeActionModes = enabled;
+ }
+
+ @Override
+ public boolean isHandleNativeActionModesEnabled() {
+ return mHandleNativeActionModes;
+ }
+
+ void endOnGoingFadeAnimation() {
+ if (mFadeAnim != null) {
+ mFadeAnim.cancel();
+ }
+ }
+
+ boolean onBackPressed() {
+ // Back cancels action modes first.
+ if (mActionMode != null) {
+ mActionMode.finish();
+ return true;
+ }
+
+ // Next collapse any expanded action views.
+ ActionBar ab = getSupportActionBar();
+ if (ab != null && ab.collapseActionView()) {
+ return true;
+ }
+
+ // Let the call through...
+ return false;
+ }
+
+ boolean onKeyShortcut(int keyCode, KeyEvent ev) {
+ // Let the Action Bar have a chance at handling the shortcut
+ ActionBar ab = getSupportActionBar();
+ if (ab != null && ab.onKeyShortcut(keyCode, ev)) {
+ return true;
+ }
+
+ // If the panel is already prepared, then perform the shortcut using it.
+ boolean handled;
+ if (mPreparedPanel != null) {
+ handled = performPanelShortcut(mPreparedPanel, ev.getKeyCode(), ev,
+ Menu.FLAG_PERFORM_NO_CLOSE);
+ if (handled) {
+ if (mPreparedPanel != null) {
+ mPreparedPanel.isHandled = true;
+ }
+ return true;
+ }
+ }
+
+ // If the panel is not prepared, then we may be trying to handle a shortcut key
+ // combination such as Control+C. Temporarily prepare the panel then mark it
+ // unprepared again when finished to ensure that the panel will again be prepared
+ // the next time it is shown for real.
+ if (mPreparedPanel == null) {
+ PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
+ preparePanel(st, ev);
+ handled = performPanelShortcut(st, ev.getKeyCode(), ev, Menu.FLAG_PERFORM_NO_CLOSE);
+ st.isPrepared = false;
+ if (handled) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
+ // If this is a MENU event, let the Activity have a go.
+ if (mOriginalWindowCallback.dispatchKeyEvent(event)) {
+ return true;
+ }
+ }
+
+ final int keyCode = event.getKeyCode();
+ final int action = event.getAction();
+ final boolean isDown = action == KeyEvent.ACTION_DOWN;
+
+ return isDown ? onKeyDown(keyCode, event) : onKeyUp(keyCode, event);
+ }
+
+ boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ onKeyUpPanel(Window.FEATURE_OPTIONS_PANEL, event);
+ return true;
+ case KeyEvent.KEYCODE_BACK:
+ final boolean wasLongPressBackDown = mLongPressBackDown;
+ mLongPressBackDown = false;
+
+ PanelFeatureState st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
+ if (st != null && st.isOpen) {
+ if (!wasLongPressBackDown) {
+ // Certain devices allow opening the options menu via a long press of the
+ // back button. We should only close the open options menu if it wasn't
+ // opened via a long press gesture.
+ closePanel(st, true);
+ }
+ return true;
+ }
+ if (onBackPressed()) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ onKeyDownPanel(Window.FEATURE_OPTIONS_PANEL, event);
+ // We need to return true here and not let it bubble up to the Window.
+ // For empty menus, PhoneWindow's KEYCODE_BACK handling will steals all events,
+ // not allowing the Activity to call onBackPressed().
+ return true;
+ case KeyEvent.KEYCODE_BACK:
+ // Certain devices allow opening the options menu via a long press of the back
+ // button. We keep a record of whether the last event is from a long press.
+ mLongPressBackDown = (event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public View createView(View parent, final String name, @NonNull Context context,
+ @NonNull AttributeSet attrs) {
+ if (mAppCompatViewInflater == null) {
+ TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
+ String viewInflaterClassName =
+ a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
+ if ((viewInflaterClassName == null)
+ || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
+ // Either default class name or set explicitly to null. In both cases
+ // create the base inflater (no reflection)
+ mAppCompatViewInflater = new AppCompatViewInflater();
+ } else {
+ try {
+ Class viewInflaterClass = Class.forName(viewInflaterClassName);
+ mAppCompatViewInflater =
+ (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
+ .newInstance();
+ } catch (Throwable t) {
+ Log.i(TAG, "Failed to instantiate custom view inflater "
+ + viewInflaterClassName + ". Falling back to default.", t);
+ mAppCompatViewInflater = new AppCompatViewInflater();
+ }
+ }
+ }
+
+ boolean inheritContext = false;
+ if (IS_PRE_LOLLIPOP) {
+ inheritContext = (attrs instanceof XmlPullParser)
+ // If we have a XmlPullParser, we can detect where we are in the layout
+ ? ((XmlPullParser) attrs).getDepth() > 1
+ // Otherwise we have to use the old heuristic
+ : shouldInheritContext((ViewParent) parent);
+ }
+
+ return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
+ IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
+ true, /* Read read app:theme as a fallback at all times for legacy reasons */
+ VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
+ );
+ }
+
+ private boolean shouldInheritContext(ViewParent parent) {
+ if (parent == null) {
+ // The initial parent is null so just return false
+ return false;
+ }
+ final View windowDecor = mWindow.getDecorView();
+ while (true) {
+ if (parent == null) {
+ // Bingo. We've hit a view which has a null parent before being terminated from
+ // the loop. This is (most probably) because it's the root view in an inflation
+ // call, therefore we should inherit. This works as the inflated layout is only
+ // added to the hierarchy at the end of the inflate() call.
+ return true;
+ } else if (parent == windowDecor || !(parent instanceof View)
+ || ViewCompat.isAttachedToWindow((View) parent)) {
+ // We have either hit the window's decor view, a parent which isn't a View
+ // (i.e. ViewRootImpl), or an attached view, so we know that the original parent
+ // is currently added to the view hierarchy. This means that it has not be
+ // inflated in the current inflate() call and we should not inherit the context.
+ return false;
+ }
+ parent = parent.getParent();
+ }
+ }
+
+ @Override
+ public void installViewFactory() {
+ LayoutInflater layoutInflater = LayoutInflater.from(mContext);
+ if (layoutInflater.getFactory() == null) {
+ LayoutInflaterCompat.setFactory2(layoutInflater, this);
+ } else {
+ if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplBase)) {
+ Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ + " so we can not install AppCompat's");
+ }
+ }
+ }
+
+ /**
+ * From {@link LayoutInflater.Factory2}.
+ */
+ @Override
+ public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+ return createView(parent, name, context, attrs);
+ }
+
+ /**
+ * From {@link LayoutInflater.Factory2}.
+ */
+ @Override
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ return onCreateView(null, name, context, attrs);
+ }
+
+ private void openPanel(final PanelFeatureState st, KeyEvent event) {
+ // Already open, return
+ if (st.isOpen || mIsDestroyed) {
+ return;
+ }
+
+ // Don't open an options panel on xlarge devices.
+ // (The app should be using an action bar for menu items.)
+ if (st.featureId == FEATURE_OPTIONS_PANEL) {
+ Configuration config = mContext.getResources().getConfiguration();
+ boolean isXLarge = (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
+ == Configuration.SCREENLAYOUT_SIZE_XLARGE;
+ if (isXLarge) {
+ return;
+ }
+ }
+
+ Window.Callback cb = getWindowCallback();
+ if ((cb != null) && (!cb.onMenuOpened(st.featureId, st.menu))) {
+ // Callback doesn't want the menu to open, reset any state
+ closePanel(st, true);
+ return;
+ }
+
+ final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ if (wm == null) {
+ return;
+ }
+
+ // Prepare panel (should have been done before, but just in case)
+ if (!preparePanel(st, event)) {
+ return;
+ }
+
+ int width = WRAP_CONTENT;
+ if (st.decorView == null || st.refreshDecorView) {
+ if (st.decorView == null) {
+ // Initialize the panel decor, this will populate st.decorView
+ if (!initializePanelDecor(st) || (st.decorView == null))
+ return;
+ } else if (st.refreshDecorView && (st.decorView.getChildCount() > 0)) {
+ // Decor needs refreshing, so remove its views
+ st.decorView.removeAllViews();
+ }
+
+ // This will populate st.shownPanelView
+ if (!initializePanelContent(st) || !st.hasPanelItems()) {
+ return;
+ }
+
+ ViewGroup.LayoutParams lp = st.shownPanelView.getLayoutParams();
+ if (lp == null) {
+ lp = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ }
+
+ int backgroundResId = st.background;
+ st.decorView.setBackgroundResource(backgroundResId);
+
+ ViewParent shownPanelParent = st.shownPanelView.getParent();
+ if (shownPanelParent != null && shownPanelParent instanceof ViewGroup) {
+ ((ViewGroup) shownPanelParent).removeView(st.shownPanelView);
+ }
+ st.decorView.addView(st.shownPanelView, lp);
+
+ /*
+ * Give focus to the view, if it or one of its children does not
+ * already have it.
+ */
+ if (!st.shownPanelView.hasFocus()) {
+ st.shownPanelView.requestFocus();
+ }
+ } else if (st.createdPanelView != null) {
+ // If we already had a panel view, carry width=MATCH_PARENT through
+ // as we did above when it was created.
+ ViewGroup.LayoutParams lp = st.createdPanelView.getLayoutParams();
+ if (lp != null && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
+ width = MATCH_PARENT;
+ }
+ }
+
+ st.isHandled = false;
+
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ width, WRAP_CONTENT,
+ st.x, st.y, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ PixelFormat.TRANSLUCENT);
+
+ lp.gravity = st.gravity;
+ lp.windowAnimations = st.windowAnimations;
+
+ wm.addView(st.decorView, lp);
+ st.isOpen = true;
+ }
+
+ private boolean initializePanelDecor(PanelFeatureState st) {
+ st.setStyle(getActionBarThemedContext());
+ st.decorView = new ListMenuDecorView(st.listPresenterContext);
+ st.gravity = Gravity.CENTER | Gravity.BOTTOM;
+ return true;
+ }
+
+ private void reopenMenu(MenuBuilder menu, boolean toggleMenuMode) {
+ if (mDecorContentParent != null && mDecorContentParent.canShowOverflowMenu()
+ && (!ViewConfiguration.get(mContext).hasPermanentMenuKey()
+ || mDecorContentParent.isOverflowMenuShowPending())) {
+
+ final Window.Callback cb = getWindowCallback();
+
+ if (!mDecorContentParent.isOverflowMenuShowing() || !toggleMenuMode) {
+ if (cb != null && !mIsDestroyed) {
+ // If we have a menu invalidation pending, do it now.
+ if (mInvalidatePanelMenuPosted &&
+ (mInvalidatePanelMenuFeatures & (1 << FEATURE_OPTIONS_PANEL)) != 0) {
+ mWindow.getDecorView().removeCallbacks(mInvalidatePanelMenuRunnable);
+ mInvalidatePanelMenuRunnable.run();
+ }
+
+ final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
+
+ // If we don't have a menu or we're waiting for a full content refresh,
+ // forget it. This is a lingering event that no longer matters.
+ if (st.menu != null && !st.refreshMenuContent &&
+ cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) {
+ cb.onMenuOpened(FEATURE_SUPPORT_ACTION_BAR, st.menu);
+ mDecorContentParent.showOverflowMenu();
+ }
+ }
+ } else {
+ mDecorContentParent.hideOverflowMenu();
+ if (!mIsDestroyed) {
+ final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
+ cb.onPanelClosed(FEATURE_SUPPORT_ACTION_BAR, st.menu);
+ }
+ }
+ return;
+ }
+
+ PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
+
+ st.refreshDecorView = true;
+ closePanel(st, false);
+
+ openPanel(st, null);
+ }
+
+ private boolean initializePanelMenu(final PanelFeatureState st) {
+ Context context = mContext;
+
+ // If we have an action bar, initialize the menu with the right theme.
+ if ((st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_SUPPORT_ACTION_BAR) &&
+ mDecorContentParent != null) {
+ final TypedValue outValue = new TypedValue();
+ final Resources.Theme baseTheme = context.getTheme();
+ baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true);
+
+ Resources.Theme widgetTheme = null;
+ if (outValue.resourceId != 0) {
+ widgetTheme = context.getResources().newTheme();
+ widgetTheme.setTo(baseTheme);
+ widgetTheme.applyStyle(outValue.resourceId, true);
+ widgetTheme.resolveAttribute(
+ R.attr.actionBarWidgetTheme, outValue, true);
+ } else {
+ baseTheme.resolveAttribute(
+ R.attr.actionBarWidgetTheme, outValue, true);
+ }
+
+ if (outValue.resourceId != 0) {
+ if (widgetTheme == null) {
+ widgetTheme = context.getResources().newTheme();
+ widgetTheme.setTo(baseTheme);
+ }
+ widgetTheme.applyStyle(outValue.resourceId, true);
+ }
+
+ if (widgetTheme != null) {
+ context = new ContextThemeWrapper(context, 0);
+ context.getTheme().setTo(widgetTheme);
+ }
+ }
+
+ final MenuBuilder menu = new MenuBuilder(context);
+ menu.setCallback(this);
+ st.setMenu(menu);
+
+ return true;
+ }
+
+ private boolean initializePanelContent(PanelFeatureState st) {
+ if (st.createdPanelView != null) {
+ st.shownPanelView = st.createdPanelView;
+ return true;
+ }
+
+ if (st.menu == null) {
+ return false;
+ }
+
+ if (mPanelMenuPresenterCallback == null) {
+ mPanelMenuPresenterCallback = new PanelMenuPresenterCallback();
+ }
+
+ MenuView menuView = st.getListMenuView(mPanelMenuPresenterCallback);
+
+ st.shownPanelView = (View) menuView;
+
+ return st.shownPanelView != null;
+ }
+
+ private boolean preparePanel(PanelFeatureState st, KeyEvent event) {
+ if (mIsDestroyed) {
+ return false;
+ }
+
+ // Already prepared (isPrepared will be reset to false later)
+ if (st.isPrepared) {
+ return true;
+ }
+
+ if ((mPreparedPanel != null) && (mPreparedPanel != st)) {
+ // Another Panel is prepared and possibly open, so close it
+ closePanel(mPreparedPanel, false);
+ }
+
+ final Window.Callback cb = getWindowCallback();
+
+ if (cb != null) {
+ st.createdPanelView = cb.onCreatePanelView(st.featureId);
+ }
+
+ final boolean isActionBarMenu =
+ (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_SUPPORT_ACTION_BAR);
+
+ if (isActionBarMenu && mDecorContentParent != null) {
+ // Enforce ordering guarantees around events so that the action bar never
+ // dispatches menu-related events before the panel is prepared.
+ mDecorContentParent.setMenuPrepared();
+ }
+
+ if (st.createdPanelView == null &&
+ (!isActionBarMenu || !(peekSupportActionBar() instanceof ToolbarActionBar))) {
+ // Since ToolbarActionBar handles the list options menu itself, we only want to
+ // init this menu panel if we're not using a TAB.
+ if (st.menu == null || st.refreshMenuContent) {
+ if (st.menu == null) {
+ if (!initializePanelMenu(st) || (st.menu == null)) {
+ return false;
+ }
+ }
+
+ if (isActionBarMenu && mDecorContentParent != null) {
+ if (mActionMenuPresenterCallback == null) {
+ mActionMenuPresenterCallback = new ActionMenuPresenterCallback();
+ }
+ mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback);
+ }
+
+ // Creating the panel menu will involve a lot of manipulation;
+ // don't dispatch change events to presenters until we're done.
+ st.menu.stopDispatchingItemsChanged();
+ if (!cb.onCreatePanelMenu(st.featureId, st.menu)) {
+ // Ditch the menu created above
+ st.setMenu(null);
+
+ if (isActionBarMenu && mDecorContentParent != null) {
+ // Don't show it in the action bar either
+ mDecorContentParent.setMenu(null, mActionMenuPresenterCallback);
+ }
+
+ return false;
+ }
+
+ st.refreshMenuContent = false;
+ }
+
+ // Preparing the panel menu can involve a lot of manipulation;
+ // don't dispatch change events to presenters until we're done.
+ st.menu.stopDispatchingItemsChanged();
+
+ // Restore action view state before we prepare. This gives apps
+ // an opportunity to override frozen/restored state in onPrepare.
+ if (st.frozenActionViewState != null) {
+ st.menu.restoreActionViewStates(st.frozenActionViewState);
+ st.frozenActionViewState = null;
+ }
+
+ // Callback and return if the callback does not want to show the menu
+ if (!cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) {
+ if (isActionBarMenu && mDecorContentParent != null) {
+ // The app didn't want to show the menu for now but it still exists.
+ // Clear it out of the action bar.
+ mDecorContentParent.setMenu(null, mActionMenuPresenterCallback);
+ }
+ st.menu.startDispatchingItemsChanged();
+ return false;
+ }
+
+ // Set the proper keymap
+ KeyCharacterMap kmap = KeyCharacterMap.load(
+ event != null ? event.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD);
+ st.qwertyMode = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC;
+ st.menu.setQwertyMode(st.qwertyMode);
+ st.menu.startDispatchingItemsChanged();
+ }
+
+ // Set other state
+ st.isPrepared = true;
+ st.isHandled = false;
+ mPreparedPanel = st;
+
+ return true;
+ }
+
+ void checkCloseActionMenu(MenuBuilder menu) {
+ if (mClosingActionMenu) {
+ return;
+ }
+
+ mClosingActionMenu = true;
+ mDecorContentParent.dismissPopups();
+ Window.Callback cb = getWindowCallback();
+ if (cb != null && !mIsDestroyed) {
+ cb.onPanelClosed(FEATURE_SUPPORT_ACTION_BAR, menu);
+ }
+ mClosingActionMenu = false;
+ }
+
+ void closePanel(int featureId) {
+ closePanel(getPanelState(featureId, true), true);
+ }
+
+ void closePanel(PanelFeatureState st, boolean doCallback) {
+ if (doCallback && st.featureId == FEATURE_OPTIONS_PANEL &&
+ mDecorContentParent != null && mDecorContentParent.isOverflowMenuShowing()) {
+ checkCloseActionMenu(st.menu);
+ return;
+ }
+
+ final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ if (wm != null && st.isOpen && st.decorView != null) {
+ wm.removeView(st.decorView);
+
+ if (doCallback) {
+ callOnPanelClosed(st.featureId, st, null);
+ }
+ }
+
+ st.isPrepared = false;
+ st.isHandled = false;
+ st.isOpen = false;
+
+ // This view is no longer shown, so null it out
+ st.shownPanelView = null;
+
+ // Next time the menu opens, it should not be in expanded mode, so
+ // force a refresh of the decor
+ st.refreshDecorView = true;
+
+ if (mPreparedPanel == st) {
+ mPreparedPanel = null;
+ }
+ }
+
+ private boolean onKeyDownPanel(int featureId, KeyEvent event) {
+ if (event.getRepeatCount() == 0) {
+ PanelFeatureState st = getPanelState(featureId, true);
+ if (!st.isOpen) {
+ return preparePanel(st, event);
+ }
+ }
+
+ return false;
+ }
+
+ private boolean onKeyUpPanel(int featureId, KeyEvent event) {
+ if (mActionMode != null) {
+ return false;
+ }
+
+ boolean handled = false;
+ final PanelFeatureState st = getPanelState(featureId, true);
+ if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null &&
+ mDecorContentParent.canShowOverflowMenu() &&
+ !ViewConfiguration.get(mContext).hasPermanentMenuKey()) {
+ if (!mDecorContentParent.isOverflowMenuShowing()) {
+ if (!mIsDestroyed && preparePanel(st, event)) {
+ handled = mDecorContentParent.showOverflowMenu();
+ }
+ } else {
+ handled = mDecorContentParent.hideOverflowMenu();
+ }
+ } else {
+ if (st.isOpen || st.isHandled) {
+ // Play the sound effect if the user closed an open menu (and not if
+ // they just released a menu shortcut)
+ handled = st.isOpen;
+ // Close menu
+ closePanel(st, true);
+ } else if (st.isPrepared) {
+ boolean show = true;
+ if (st.refreshMenuContent) {
+ // Something may have invalidated the menu since we prepared it.
+ // Re-prepare it to refresh.
+ st.isPrepared = false;
+ show = preparePanel(st, event);
+ }
+
+ if (show) {
+ // Show menu
+ openPanel(st, event);
+ handled = true;
+ }
+ }
+ }
+
+ if (handled) {
+ AudioManager audioManager = (AudioManager) mContext.getSystemService(
+ Context.AUDIO_SERVICE);
+ if (audioManager != null) {
+ audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
+ } else {
+ Log.w(TAG, "Couldn't get audio manager");
+ }
+ }
+ return handled;
+ }
+
+ void callOnPanelClosed(int featureId, PanelFeatureState panel, Menu menu) {
+ // Try to get a menu
+ if (menu == null) {
+ // Need a panel to grab the menu, so try to get that
+ if (panel == null) {
+ if ((featureId >= 0) && (featureId < mPanels.length)) {
+ panel = mPanels[featureId];
+ }
+ }
+
+ if (panel != null) {
+ // menu still may be null, which is okay--we tried our best
+ menu = panel.menu;
+ }
+ }
+
+ // If the panel is not open, do not callback
+ if ((panel != null) && (!panel.isOpen))
+ return;
+
+ if (!mIsDestroyed) {
+ // We need to be careful which callback we dispatch the call to. We can not dispatch
+ // this to the Window's callback since that will call back into this method and cause a
+ // crash. Instead we need to dispatch down to the original Activity/Dialog/etc.
+ mOriginalWindowCallback.onPanelClosed(featureId, menu);
+ }
+ }
+
+ PanelFeatureState findMenuPanel(Menu menu) {
+ final PanelFeatureState[] panels = mPanels;
+ final int N = panels != null ? panels.length : 0;
+ for (int i = 0; i < N; i++) {
+ final PanelFeatureState panel = panels[i];
+ if (panel != null && panel.menu == menu) {
+ return panel;
+ }
+ }
+ return null;
+ }
+
+ protected PanelFeatureState getPanelState(int featureId, boolean required) {
+ PanelFeatureState[] ar;
+ if ((ar = mPanels) == null || ar.length <= featureId) {
+ PanelFeatureState[] nar = new PanelFeatureState[featureId + 1];
+ if (ar != null) {
+ System.arraycopy(ar, 0, nar, 0, ar.length);
+ }
+ mPanels = ar = nar;
+ }
+
+ PanelFeatureState st = ar[featureId];
+ if (st == null) {
+ ar[featureId] = st = new PanelFeatureState(featureId);
+ }
+ return st;
+ }
+
+ private boolean performPanelShortcut(PanelFeatureState st, int keyCode, KeyEvent event,
+ int flags) {
+ if (event.isSystem()) {
+ return false;
+ }
+
+ boolean handled = false;
+
+ // Only try to perform menu shortcuts if preparePanel returned true (possible false
+ // return value from application not wanting to show the menu).
+ if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) {
+ // The menu is prepared now, perform the shortcut on it
+ handled = st.menu.performShortcut(keyCode, event, flags);
+ }
+
+ if (handled) {
+ // Only close down the menu if we don't have an action bar keeping it open.
+ if ((flags & Menu.FLAG_PERFORM_NO_CLOSE) == 0 && mDecorContentParent == null) {
+ closePanel(st, true);
+ }
+ }
+
+ return handled;
+ }
+
+ private void invalidatePanelMenu(int featureId) {
+ mInvalidatePanelMenuFeatures |= 1 << featureId;
+
+ if (!mInvalidatePanelMenuPosted) {
+ ViewCompat.postOnAnimation(mWindow.getDecorView(), mInvalidatePanelMenuRunnable);
+ mInvalidatePanelMenuPosted = true;
+ }
+ }
+
+ void doInvalidatePanelMenu(int featureId) {
+ PanelFeatureState st = getPanelState(featureId, true);
+ Bundle savedActionViewStates = null;
+ if (st.menu != null) {
+ savedActionViewStates = new Bundle();
+ st.menu.saveActionViewStates(savedActionViewStates);
+ if (savedActionViewStates.size() > 0) {
+ st.frozenActionViewState = savedActionViewStates;
+ }
+ // This will be started again when the panel is prepared.
+ st.menu.stopDispatchingItemsChanged();
+ st.menu.clear();
+ }
+ st.refreshMenuContent = true;
+ st.refreshDecorView = true;
+
+ // Prepare the options panel if we have an action bar
+ if ((featureId == FEATURE_SUPPORT_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL)
+ && mDecorContentParent != null) {
+ st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
+ if (st != null) {
+ st.isPrepared = false;
+ preparePanel(st, null);
+ }
+ }
+ }
+
+ /**
+ * Updates the status bar guard
+ *
+ * @param insetTop the current top system window inset
+ * @return the new top system window inset
+ */
+ int updateStatusGuard(int insetTop) {
+ boolean showStatusGuard = false;
+ // Show the status guard when the non-overlay contextual action bar is showing
+ if (mActionModeView != null) {
+ if (mActionModeView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
+ ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)
+ mActionModeView.getLayoutParams();
+ boolean mlpChanged = false;
+
+ if (mActionModeView.isShown()) {
+ if (mTempRect1 == null) {
+ mTempRect1 = new Rect();
+ mTempRect2 = new Rect();
+ }
+ final Rect insets = mTempRect1;
+ final Rect localInsets = mTempRect2;
+ insets.set(0, insetTop, 0, 0);
+
+ ViewUtils.computeFitSystemWindows(mSubDecor, insets, localInsets);
+ final int newMargin = localInsets.top == 0 ? insetTop : 0;
+ if (mlp.topMargin != newMargin) {
+ mlpChanged = true;
+ mlp.topMargin = insetTop;
+
+ if (mStatusGuard == null) {
+ mStatusGuard = new View(mContext);
+ mStatusGuard.setBackgroundColor(mContext.getResources()
+ .getColor(R.color.abc_input_method_navigation_guard));
+ mSubDecor.addView(mStatusGuard, -1,
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ insetTop));
+ } else {
+ ViewGroup.LayoutParams lp = mStatusGuard.getLayoutParams();
+ if (lp.height != insetTop) {
+ lp.height = insetTop;
+ mStatusGuard.setLayoutParams(lp);
+ }
+ }
+ }
+
+ // The action mode's theme may differ from the app, so
+ // always show the status guard above it.
+ showStatusGuard = mStatusGuard != null;
+
+ // We only need to consume the insets if the action
+ // mode is overlaid on the app content (e.g. it's
+ // sitting in a FrameLayout, see
+ // screen_simple_overlay_action_mode.xml).
+ if (!mOverlayActionMode && showStatusGuard) {
+ insetTop = 0;
+ }
+ } else {
+ // reset top margin
+ if (mlp.topMargin != 0) {
+ mlpChanged = true;
+ mlp.topMargin = 0;
+ }
+ }
+ if (mlpChanged) {
+ mActionModeView.setLayoutParams(mlp);
+ }
+ }
+ }
+ if (mStatusGuard != null) {
+ mStatusGuard.setVisibility(showStatusGuard ? View.VISIBLE : View.GONE);
+ }
+
+ return insetTop;
+ }
+
+ private void throwFeatureRequestIfSubDecorInstalled() {
+ if (mSubDecorInstalled) {
+ throw new AndroidRuntimeException(
+ "Window feature must be requested before adding content");
+ }
+ }
+
+ private int sanitizeWindowFeatureId(int featureId) {
+ if (featureId == WindowCompat.FEATURE_ACTION_BAR) {
+ Log.i(TAG, "You should now use the AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR"
+ + " id when requesting this feature.");
+ return FEATURE_SUPPORT_ACTION_BAR;
+ } else if (featureId == WindowCompat.FEATURE_ACTION_BAR_OVERLAY) {
+ Log.i(TAG, "You should now use the AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR_OVERLAY"
+ + " id when requesting this feature.");
+ return FEATURE_SUPPORT_ACTION_BAR_OVERLAY;
+ }
+ // Else we'll just return the original id
+ return featureId;
+ }
+
+ ViewGroup getSubDecor() {
+ return mSubDecor;
+ }
+
+ void dismissPopups() {
+ if (mDecorContentParent != null) {
+ mDecorContentParent.dismissPopups();
+ }
+
+ if (mActionModePopup != null) {
+ mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
+ if (mActionModePopup.isShowing()) {
+ try {
+ mActionModePopup.dismiss();
+ } catch (IllegalArgumentException e) {
+ // Pre-v18, there are times when the Window will remove the popup before us.
+ // In these cases we need to swallow the resulting exception.
+ }
+ }
+ mActionModePopup = null;
+ }
+ endOnGoingFadeAnimation();
+
+ PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
+ if (st != null && st.menu != null) {
+ st.menu.close();
+ }
+ }
+
+ @Override
+ public boolean applyDayNight() {
+ boolean applied = false;
+
+ @NightMode final int nightMode = getNightMode();
+ @ApplyableNightMode final int modeToApply = mapNightMode(nightMode);
+ if (modeToApply != MODE_NIGHT_FOLLOW_SYSTEM) {
+ applied = updateForNightMode(modeToApply);
+ }
+
+ if (nightMode == MODE_NIGHT_AUTO) {
+ // If we're already been started, we may need to setup auto mode again
+ ensureAutoNightModeManager();
+ mAutoNightModeManager.setup();
+ }
+
+ mApplyDayNightCalled = true;
+ return applied;
+ }
+
+ @Override
+ public void setLocalNightMode(@NightMode final int mode) {
+ switch (mode) {
+ case MODE_NIGHT_AUTO:
+ case MODE_NIGHT_NO:
+ case MODE_NIGHT_YES:
+ case MODE_NIGHT_FOLLOW_SYSTEM:
+ if (mLocalNightMode != mode) {
+ mLocalNightMode = mode;
+ if (mApplyDayNightCalled) {
+ // If we've already applied day night, re-apply since we won't be
+ // called again
+ applyDayNight();
+ }
+ }
+ break;
+ default:
+ Log.i(TAG, "setLocalNightMode() called with an unknown mode");
+ break;
+ }
+ }
+
+ @ApplyableNightMode
+ int mapNightMode(@NightMode final int mode) {
+ switch (mode) {
+ case MODE_NIGHT_AUTO:
+ ensureAutoNightModeManager();
+ return mAutoNightModeManager.getApplyableNightMode();
+ case MODE_NIGHT_UNSPECIFIED:
+ // If we don't have a mode specified, just let the system handle it
+ return MODE_NIGHT_FOLLOW_SYSTEM;
+ default:
+ return mode;
+ }
+ }
+
+ @NightMode
+ private int getNightMode() {
+ return mLocalNightMode != MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode();
+ }
+
+ /**
+ * Updates the {@link Resources} configuration {@code uiMode} with the
+ * chosen {@code UI_MODE_NIGHT} value.
+ */
+ private boolean updateForNightMode(@ApplyableNightMode final int mode) {
+ final Resources res = mContext.getResources();
+ final Configuration conf = res.getConfiguration();
+ final int currentNightMode = conf.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+
+ final int newNightMode = (mode == MODE_NIGHT_YES)
+ ? Configuration.UI_MODE_NIGHT_YES
+ : Configuration.UI_MODE_NIGHT_NO;
+
+ if (currentNightMode != newNightMode) {
+ if (shouldRecreateOnNightModeChange()) {
+ if (DEBUG) {
+ Log.d(TAG, "applyNightMode() | Night mode changed, recreating Activity");
+ }
+ // If we've already been created, we need to recreate the Activity for the
+ // mode to be applied
+ final Activity activity = (Activity) mContext;
+ activity.recreate();
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "applyNightMode() | Night mode changed, updating configuration");
+ }
+ final Configuration config = new Configuration(conf);
+ final DisplayMetrics metrics = res.getDisplayMetrics();
+
+ // Update the UI Mode to reflect the new night mode
+ config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
+ res.updateConfiguration(config, metrics);
+
+ // We may need to flush the Resources' drawable cache due to framework bugs.
+ if (!(Build.VERSION.SDK_INT >= 26)) {
+ ResourcesFlusher.flush(res);
+ }
+ }
+ return true;
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "applyNightMode() | Skipping. Night mode has not changed: " + mode);
+ }
+ }
+ return false;
+ }
+
+ private void ensureAutoNightModeManager() {
+ if (mAutoNightModeManager == null) {
+ mAutoNightModeManager = new AutoNightModeManager(TwilightManager.getInstance(mContext));
+ }
+ }
+
+ @VisibleForTesting
+ final AutoNightModeManager getAutoNightModeManager() {
+ ensureAutoNightModeManager();
+ return mAutoNightModeManager;
+ }
+
+ private boolean shouldRecreateOnNightModeChange() {
+ if (mApplyDayNightCalled && mContext instanceof Activity) {
+ // If we've already applyDayNight() (via setTheme), we need to check if the
+ // Activity has configChanges set to handle uiMode changes
+ final PackageManager pm = mContext.getPackageManager();
+ try {
+ final ActivityInfo info = pm.getActivityInfo(
+ new ComponentName(mContext, mContext.getClass()), 0);
+ // We should return true (to recreate) if configChanges does not want to
+ // handle uiMode
+ return (info.configChanges & ActivityInfo.CONFIG_UI_MODE) == 0;
+ } catch (PackageManager.NameNotFoundException e) {
+ // This shouldn't happen but let's not crash because of it, we'll just log and
+ // return true (since most apps will do that anyway)
+ Log.d(TAG, "Exception while getting ActivityInfo", e);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Clears out internal reference when the action mode is destroyed.
+ */
+ class ActionModeCallbackWrapperV9 implements ActionMode.Callback {
+ private ActionMode.Callback mWrapped;
+
+ public ActionModeCallbackWrapperV9(ActionMode.Callback wrapped) {
+ mWrapped = wrapped;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return mWrapped.onCreateActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return mWrapped.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return mWrapped.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mWrapped.onDestroyActionMode(mode);
+ if (mActionModePopup != null) {
+ mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
+ }
+
+ if (mActionModeView != null) {
+ endOnGoingFadeAnimation();
+ mFadeAnim = ViewCompat.animate(mActionModeView).alpha(0f);
+ mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(View view) {
+ mActionModeView.setVisibility(View.GONE);
+ if (mActionModePopup != null) {
+ mActionModePopup.dismiss();
+ } else if (mActionModeView.getParent() instanceof View) {
+ ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
+ }
+ mActionModeView.removeAllViews();
+ mFadeAnim.setListener(null);
+ mFadeAnim = null;
+ }
+ });
+ }
+ if (mAppCompatCallback != null) {
+ mAppCompatCallback.onSupportActionModeFinished(mActionMode);
+ }
+ mActionMode = null;
+ }
+ }
+
+ private final class PanelMenuPresenterCallback implements MenuPresenter.Callback {
+ PanelMenuPresenterCallback() {
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ final Menu parentMenu = menu.getRootMenu();
+ final boolean isSubMenu = parentMenu != menu;
+ final PanelFeatureState panel = findMenuPanel(isSubMenu ? parentMenu : menu);
+ if (panel != null) {
+ if (isSubMenu) {
+ callOnPanelClosed(panel.featureId, panel, parentMenu);
+ closePanel(panel, true);
+ } else {
+ // Close the panel and only do the callback if the menu is being
+ // closed completely, not if opening a sub menu
+ closePanel(panel, allMenusAreClosing);
+ }
+ }
+ }
+
+ @Override
+ public boolean onOpenSubMenu(MenuBuilder subMenu) {
+ if (subMenu == null && mHasActionBar) {
+ Window.Callback cb = getWindowCallback();
+ if (cb != null && !mIsDestroyed) {
+ cb.onMenuOpened(FEATURE_SUPPORT_ACTION_BAR, subMenu);
+ }
+ }
+ return true;
+ }
+ }
+
+ private final class ActionMenuPresenterCallback implements MenuPresenter.Callback {
+ ActionMenuPresenterCallback() {
+ }
+
+ @Override
+ public boolean onOpenSubMenu(MenuBuilder subMenu) {
+ Window.Callback cb = getWindowCallback();
+ if (cb != null) {
+ cb.onMenuOpened(FEATURE_SUPPORT_ACTION_BAR, subMenu);
+ }
+ return true;
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+ checkCloseActionMenu(menu);
+ }
+ }
+
+ protected static final class PanelFeatureState {
+
+ /** Feature ID for this panel. */
+ int featureId;
+
+ int background;
+
+ int gravity;
+
+ int x;
+
+ int y;
+
+ int windowAnimations;
+
+ /** Dynamic state of the panel. */
+ ViewGroup decorView;
+
+ /** The panel that we are actually showing. */
+ View shownPanelView;
+
+ /** The panel that was returned by onCreatePanelView(). */
+ View createdPanelView;
+
+ /** Use {@link #setMenu} to set this. */
+ MenuBuilder menu;
+
+ ListMenuPresenter listMenuPresenter;
+
+ Context listPresenterContext;
+
+ /**
+ * Whether the panel has been prepared (see
+ * {@link #preparePanel}).
+ */
+ boolean isPrepared;
+
+ /**
+ * Whether an item's action has been performed. This happens in obvious
+ * scenarios (user clicks on menu item), but can also happen with
+ * chording menu+(shortcut key).
+ */
+ boolean isHandled;
+
+ boolean isOpen;
+
+ public boolean qwertyMode;
+
+ boolean refreshDecorView;
+
+ boolean refreshMenuContent;
+
+ boolean wasLastOpen;
+
+ /**
+ * Contains the state of the menu when told to freeze.
+ */
+ Bundle frozenMenuState;
+
+ /**
+ * Contains the state of associated action views when told to freeze.
+ * These are saved across invalidations.
+ */
+ Bundle frozenActionViewState;
+
+ PanelFeatureState(int featureId) {
+ this.featureId = featureId;
+
+ refreshDecorView = false;
+ }
+
+ public boolean hasPanelItems() {
+ if (shownPanelView == null) return false;
+ if (createdPanelView != null) return true;
+
+ return listMenuPresenter.getAdapter().getCount() > 0;
+ }
+
+ /**
+ * Unregister and free attached MenuPresenters. They will be recreated as needed.
+ */
+ public void clearMenuPresenters() {
+ if (menu != null) {
+ menu.removeMenuPresenter(listMenuPresenter);
+ }
+ listMenuPresenter = null;
+ }
+
+ void setStyle(Context context) {
+ final TypedValue outValue = new TypedValue();
+ final Resources.Theme widgetTheme = context.getResources().newTheme();
+ widgetTheme.setTo(context.getTheme());
+
+ // First apply the actionBarPopupTheme
+ widgetTheme.resolveAttribute(R.attr.actionBarPopupTheme, outValue, true);
+ if (outValue.resourceId != 0) {
+ widgetTheme.applyStyle(outValue.resourceId, true);
+ }
+
+ // Now apply the panelMenuListTheme
+ widgetTheme.resolveAttribute(R.attr.panelMenuListTheme, outValue, true);
+ if (outValue.resourceId != 0) {
+ widgetTheme.applyStyle(outValue.resourceId, true);
+ } else {
+ widgetTheme.applyStyle(R.style.Theme_AppCompat_CompactMenu, true);
+ }
+
+ context = new ContextThemeWrapper(context, 0);
+ context.getTheme().setTo(widgetTheme);
+
+ listPresenterContext = context;
+
+ TypedArray a = context.obtainStyledAttributes(R.styleable.AppCompatTheme);
+ background = a.getResourceId(
+ R.styleable.AppCompatTheme_panelBackground, 0);
+ windowAnimations = a.getResourceId(
+ R.styleable.AppCompatTheme_android_windowAnimationStyle, 0);
+ a.recycle();
+ }
+
+ void setMenu(MenuBuilder menu) {
+ if (menu == this.menu) return;
+
+ if (this.menu != null) {
+ this.menu.removeMenuPresenter(listMenuPresenter);
+ }
+ this.menu = menu;
+ if (menu != null) {
+ if (listMenuPresenter != null) menu.addMenuPresenter(listMenuPresenter);
+ }
+ }
+
+ MenuView getListMenuView(MenuPresenter.Callback cb) {
+ if (menu == null) return null;
+
+ if (listMenuPresenter == null) {
+ listMenuPresenter = new ListMenuPresenter(listPresenterContext,
+ R.layout.abc_list_menu_item_layout);
+ listMenuPresenter.setCallback(cb);
+ menu.addMenuPresenter(listMenuPresenter);
+ }
+
+ MenuView result = listMenuPresenter.getMenuView(decorView);
+
+ return result;
+ }
+
+ Parcelable onSaveInstanceState() {
+ SavedState savedState = new SavedState();
+ savedState.featureId = featureId;
+ savedState.isOpen = isOpen;
+
+ if (menu != null) {
+ savedState.menuState = new Bundle();
+ menu.savePresenterStates(savedState.menuState);
+ }
+
+ return savedState;
+ }
+
+ void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ featureId = savedState.featureId;
+ wasLastOpen = savedState.isOpen;
+ frozenMenuState = savedState.menuState;
+
+ shownPanelView = null;
+ decorView = null;
+ }
+
+ void applyFrozenState() {
+ if (menu != null && frozenMenuState != null) {
+ menu.restorePresenterStates(frozenMenuState);
+ frozenMenuState = null;
+ }
+ }
+
+ private static class SavedState implements Parcelable {
+ int featureId;
+ boolean isOpen;
+ Bundle menuState;
+
+ SavedState() {
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(featureId);
+ dest.writeInt(isOpen ? 1 : 0);
+
+ if (isOpen) {
+ dest.writeBundle(menuState);
+ }
+ }
+
+ static SavedState readFromParcel(Parcel source, ClassLoader loader) {
+ SavedState savedState = new SavedState();
+ savedState.featureId = source.readInt();
+ savedState.isOpen = source.readInt() == 1;
+
+ if (savedState.isOpen) {
+ savedState.menuState = source.readBundle(loader);
+ }
+
+ return savedState;
+ }
+
+ public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return readFromParcel(in, loader);
+ }
+
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return readFromParcel(in, null);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+ }
+
+ private class ListMenuDecorView extends ContentFrameLayout {
+ public ListMenuDecorView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return AppCompatDelegateImplBase.this.dispatchKeyEvent(event)
+ || super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_DOWN) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ if (isOutOfBounds(x, y)) {
+ closePanel(Window.FEATURE_OPTIONS_PANEL);
+ return true;
+ }
+ }
+ return super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public void setBackgroundResource(int resid) {
+ setBackgroundDrawable(AppCompatResources.getDrawable(getContext(), resid));
+ }
+
+ private boolean isOutOfBounds(int x, int y) {
+ return x < -5 || y < -5 || x > (getWidth() + 5) || y > (getHeight() + 5);
+ }
+ }
+
+
+ class AppCompatWindowCallbackV14 extends WindowCallbackWrapper {
+ AppCompatWindowCallbackV14(Window.Callback callback) {
super(callback);
}
@@ -369,14 +2557,154 @@
@Override
public boolean onMenuOpened(int featureId, Menu menu) {
super.onMenuOpened(featureId, menu);
- AppCompatDelegateImplBase.this.onMenuOpened(featureId, menu);
+ AppCompatDelegateImplBase.this.onMenuOpened(featureId);
return true;
}
@Override
public void onPanelClosed(int featureId, Menu menu) {
super.onPanelClosed(featureId, menu);
- AppCompatDelegateImplBase.this.onPanelClosed(featureId, menu);
+ AppCompatDelegateImplBase.this.onPanelClosed(featureId);
+ }
+
+ @Override
+ public android.view.ActionMode onWindowStartingActionMode(
+ android.view.ActionMode.Callback callback) {
+ // We wrap in a support action mode on v14+ if enabled
+ if (isHandleNativeActionModesEnabled()) {
+ return startAsSupportActionMode(callback);
+ }
+ // Else, let the call fall through to the wrapped callback
+ return super.onWindowStartingActionMode(callback);
+ }
+
+ /**
+ * Wrap the framework {@link android.view.ActionMode.Callback} in a support action mode and
+ * let AppCompat display it.
+ */
+ final android.view.ActionMode startAsSupportActionMode(
+ android.view.ActionMode.Callback callback) {
+ // Wrap the callback as a v7 ActionMode.Callback
+ final SupportActionModeWrapper.CallbackWrapper callbackWrapper =
+ new SupportActionModeWrapper.CallbackWrapper(mContext, callback);
+
+ // Try and start a support action mode using the wrapped callback
+ final androidx.appcompat.view.ActionMode supportActionMode =
+ startSupportActionMode(callbackWrapper);
+
+ if (supportActionMode != null) {
+ // If we received a support action mode, wrap and return it
+ return callbackWrapper.getActionModeWrapper(supportActionMode);
+ }
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ final class AutoNightModeManager {
+ private TwilightManager mTwilightManager;
+ private boolean mIsNight;
+
+ private BroadcastReceiver mAutoTimeChangeReceiver;
+ private IntentFilter mAutoTimeChangeReceiverFilter;
+
+ AutoNightModeManager(@NonNull TwilightManager twilightManager) {
+ mTwilightManager = twilightManager;
+ mIsNight = twilightManager.isNight();
+ }
+
+ @ApplyableNightMode
+ int getApplyableNightMode() {
+ mIsNight = mTwilightManager.isNight();
+ return mIsNight ? MODE_NIGHT_YES : MODE_NIGHT_NO;
+ }
+
+ void dispatchTimeChanged() {
+ final boolean isNight = mTwilightManager.isNight();
+ if (isNight != mIsNight) {
+ mIsNight = isNight;
+ applyDayNight();
+ }
+ }
+
+ void setup() {
+ cleanup();
+
+ // If we're set to AUTO, we register a receiver to be notified on time changes. The
+ // system only sends the tick out every minute, but that's enough fidelity for our use
+ // case
+ if (mAutoTimeChangeReceiver == null) {
+ mAutoTimeChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Log.d("AutoTimeChangeReceiver", "onReceive | Intent: " + intent);
+ }
+ dispatchTimeChanged();
+ }
+ };
+ }
+ if (mAutoTimeChangeReceiverFilter == null) {
+ mAutoTimeChangeReceiverFilter = new IntentFilter();
+ mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_CHANGED);
+ mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_TICK);
+ }
+ mContext.registerReceiver(mAutoTimeChangeReceiver, mAutoTimeChangeReceiverFilter);
+ }
+
+ void cleanup() {
+ if (mAutoTimeChangeReceiver != null) {
+ mContext.unregisterReceiver(mAutoTimeChangeReceiver);
+ mAutoTimeChangeReceiver = null;
+ }
+ }
+ }
+
+ @Override
+ public final ActionBarDrawerToggle.Delegate getDrawerToggleDelegate() {
+ return new ActionBarDrawableToggleImpl();
+ }
+
+ private class ActionBarDrawableToggleImpl implements ActionBarDrawerToggle.Delegate {
+ ActionBarDrawableToggleImpl() {
+ }
+
+ @Override
+ public Drawable getThemeUpIndicator() {
+ final TintTypedArray a = TintTypedArray.obtainStyledAttributes(
+ getActionBarThemedContext(), null, new int[]{ R.attr.homeAsUpIndicator });
+ final Drawable result = a.getDrawable(0);
+ a.recycle();
+ return result;
+ }
+
+ @Override
+ public Context getActionBarThemedContext() {
+ return AppCompatDelegateImplBase.this.getActionBarThemedContext();
+ }
+
+ @Override
+ public boolean isNavigationVisible() {
+ final ActionBar ab = getSupportActionBar();
+ return ab != null && (ab.getDisplayOptions() & ActionBar.DISPLAY_HOME_AS_UP) != 0;
+ }
+
+ @Override
+ public void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) {
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.setHomeAsUpIndicator(upDrawable);
+ ab.setHomeActionContentDescription(contentDescRes);
+ }
+ }
+
+ @Override
+ public void setActionBarDescription(int contentDescRes) {
+ ActionBar ab = getSupportActionBar();
+ if (ab != null) {
+ ab.setHomeActionContentDescription(contentDescRes);
+ }
}
}
}
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV14.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV14.java
deleted file mode 100644
index 67412f4..0000000
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV14.java
+++ /dev/null
@@ -1,377 +0,0 @@
-/*
- * Copyright (C) 2015 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 androidx.appcompat.app;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Build;
-import android.os.Bundle;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.ActionMode;
-import android.view.View;
-import android.view.Window;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
-import androidx.appcompat.view.SupportActionModeWrapper;
-
-@RequiresApi(14)
-class AppCompatDelegateImplV14 extends AppCompatDelegateImplV9 {
-
- private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode";
-
- @NightMode
- private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED;
- private boolean mApplyDayNightCalled;
-
- private boolean mHandleNativeActionModes = true; // defaults to true
-
- private AutoNightModeManager mAutoNightModeManager;
-
- AppCompatDelegateImplV14(Context context, Window window, AppCompatCallback callback) {
- super(context, window, callback);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (savedInstanceState != null && mLocalNightMode == MODE_NIGHT_UNSPECIFIED) {
- // If we have a icicle and we haven't had a local night mode set yet, try and read
- // it from the icicle
- mLocalNightMode = savedInstanceState.getInt(KEY_LOCAL_NIGHT_MODE,
- MODE_NIGHT_UNSPECIFIED);
- }
- }
-
- @Override
- public boolean hasWindowFeature(int featureId) {
- return super.hasWindowFeature(featureId) || mWindow.hasFeature(featureId);
- }
-
- @Override
- View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
- // On Honeycomb+, Activity's private inflater factory will handle calling its
- // onCreateView(...)
- return null;
- }
-
- @Override
- Window.Callback wrapWindowCallback(Window.Callback callback) {
- // Override the window callback so that we can intercept onWindowStartingActionMode()
- // calls
- return new AppCompatWindowCallbackV14(callback);
- }
-
- @Override
- public void setHandleNativeActionModesEnabled(boolean enabled) {
- mHandleNativeActionModes = enabled;
- }
-
- @Override
- public boolean isHandleNativeActionModesEnabled() {
- return mHandleNativeActionModes;
- }
-
- @Override
- public boolean applyDayNight() {
- boolean applied = false;
-
- @NightMode final int nightMode = getNightMode();
- @ApplyableNightMode final int modeToApply = mapNightMode(nightMode);
- if (modeToApply != MODE_NIGHT_FOLLOW_SYSTEM) {
- applied = updateForNightMode(modeToApply);
- }
-
- if (nightMode == MODE_NIGHT_AUTO) {
- // If we're already been started, we may need to setup auto mode again
- ensureAutoNightModeManager();
- mAutoNightModeManager.setup();
- }
-
- mApplyDayNightCalled = true;
- return applied;
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- // This will apply day/night if the time has changed, it will also call through to
- // setupAutoNightModeIfNeeded()
- applyDayNight();
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // Make sure we clean up any receivers setup for AUTO mode
- if (mAutoNightModeManager != null) {
- mAutoNightModeManager.cleanup();
- }
- }
-
- @Override
- public void setLocalNightMode(@NightMode final int mode) {
- switch (mode) {
- case MODE_NIGHT_AUTO:
- case MODE_NIGHT_NO:
- case MODE_NIGHT_YES:
- case MODE_NIGHT_FOLLOW_SYSTEM:
- if (mLocalNightMode != mode) {
- mLocalNightMode = mode;
- if (mApplyDayNightCalled) {
- // If we've already applied day night, re-apply since we won't be
- // called again
- applyDayNight();
- }
- }
- break;
- default:
- Log.i(TAG, "setLocalNightMode() called with an unknown mode");
- break;
- }
- }
-
- @ApplyableNightMode
- int mapNightMode(@NightMode final int mode) {
- switch (mode) {
- case MODE_NIGHT_AUTO:
- ensureAutoNightModeManager();
- return mAutoNightModeManager.getApplyableNightMode();
- case MODE_NIGHT_UNSPECIFIED:
- // If we don't have a mode specified, just let the system handle it
- return MODE_NIGHT_FOLLOW_SYSTEM;
- default:
- return mode;
- }
- }
-
- @NightMode
- private int getNightMode() {
- return mLocalNightMode != MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode();
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED) {
- // If we have a local night mode set, save it
- outState.putInt(KEY_LOCAL_NIGHT_MODE, mLocalNightMode);
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
-
- // Make sure we clean up any receivers setup for AUTO mode
- if (mAutoNightModeManager != null) {
- mAutoNightModeManager.cleanup();
- }
- }
-
- /**
- * Updates the {@link Resources} configuration {@code uiMode} with the
- * chosen {@code UI_MODE_NIGHT} value.
- */
- private boolean updateForNightMode(@ApplyableNightMode final int mode) {
- final Resources res = mContext.getResources();
- final Configuration conf = res.getConfiguration();
- final int currentNightMode = conf.uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
- final int newNightMode = (mode == MODE_NIGHT_YES)
- ? Configuration.UI_MODE_NIGHT_YES
- : Configuration.UI_MODE_NIGHT_NO;
-
- if (currentNightMode != newNightMode) {
- if (shouldRecreateOnNightModeChange()) {
- if (DEBUG) {
- Log.d(TAG, "applyNightMode() | Night mode changed, recreating Activity");
- }
- // If we've already been created, we need to recreate the Activity for the
- // mode to be applied
- final Activity activity = (Activity) mContext;
- activity.recreate();
- } else {
- if (DEBUG) {
- Log.d(TAG, "applyNightMode() | Night mode changed, updating configuration");
- }
- final Configuration config = new Configuration(conf);
- final DisplayMetrics metrics = res.getDisplayMetrics();
-
- // Update the UI Mode to reflect the new night mode
- config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
- res.updateConfiguration(config, metrics);
-
- // We may need to flush the Resources' drawable cache due to framework bugs.
- if (!(Build.VERSION.SDK_INT >= 26)) {
- ResourcesFlusher.flush(res);
- }
- }
- return true;
- } else {
- if (DEBUG) {
- Log.d(TAG, "applyNightMode() | Skipping. Night mode has not changed: " + mode);
- }
- }
- return false;
- }
-
- private void ensureAutoNightModeManager() {
- if (mAutoNightModeManager == null) {
- mAutoNightModeManager = new AutoNightModeManager(TwilightManager.getInstance(mContext));
- }
- }
-
- @VisibleForTesting
- final AutoNightModeManager getAutoNightModeManager() {
- ensureAutoNightModeManager();
- return mAutoNightModeManager;
- }
-
- private boolean shouldRecreateOnNightModeChange() {
- if (mApplyDayNightCalled && mContext instanceof Activity) {
- // If we've already applyDayNight() (via setTheme), we need to check if the
- // Activity has configChanges set to handle uiMode changes
- final PackageManager pm = mContext.getPackageManager();
- try {
- final ActivityInfo info = pm.getActivityInfo(
- new ComponentName(mContext, mContext.getClass()), 0);
- // We should return true (to recreate) if configChanges does not want to
- // handle uiMode
- return (info.configChanges & ActivityInfo.CONFIG_UI_MODE) == 0;
- } catch (PackageManager.NameNotFoundException e) {
- // This shouldn't happen but let's not crash because of it, we'll just log and
- // return true (since most apps will do that anyway)
- Log.d(TAG, "Exception while getting ActivityInfo", e);
- return true;
- }
- }
- return false;
- }
-
- class AppCompatWindowCallbackV14 extends AppCompatWindowCallbackBase {
- AppCompatWindowCallbackV14(Window.Callback callback) {
- super(callback);
- }
-
- @Override
- public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
- // We wrap in a support action mode on v14+ if enabled
- if (isHandleNativeActionModesEnabled()) {
- return startAsSupportActionMode(callback);
- }
- // Else, let the call fall through to the wrapped callback
- return super.onWindowStartingActionMode(callback);
- }
-
- /**
- * Wrap the framework {@link ActionMode.Callback} in a support action mode and
- * let AppCompat display it.
- */
- final ActionMode startAsSupportActionMode(ActionMode.Callback callback) {
- // Wrap the callback as a v7 ActionMode.Callback
- final SupportActionModeWrapper.CallbackWrapper callbackWrapper
- = new SupportActionModeWrapper.CallbackWrapper(mContext, callback);
-
- // Try and start a support action mode using the wrapped callback
- final androidx.appcompat.view.ActionMode supportActionMode
- = startSupportActionMode(callbackWrapper);
-
- if (supportActionMode != null) {
- // If we received a support action mode, wrap and return it
- return callbackWrapper.getActionModeWrapper(supportActionMode);
- }
- return null;
- }
- }
-
- @VisibleForTesting
- final class AutoNightModeManager {
- private TwilightManager mTwilightManager;
- private boolean mIsNight;
-
- private BroadcastReceiver mAutoTimeChangeReceiver;
- private IntentFilter mAutoTimeChangeReceiverFilter;
-
- AutoNightModeManager(@NonNull TwilightManager twilightManager) {
- mTwilightManager = twilightManager;
- mIsNight = twilightManager.isNight();
- }
-
- @ApplyableNightMode
- final int getApplyableNightMode() {
- mIsNight = mTwilightManager.isNight();
- return mIsNight ? MODE_NIGHT_YES : MODE_NIGHT_NO;
- }
-
- final void dispatchTimeChanged() {
- final boolean isNight = mTwilightManager.isNight();
- if (isNight != mIsNight) {
- mIsNight = isNight;
- applyDayNight();
- }
- }
-
- final void setup() {
- cleanup();
-
- // If we're set to AUTO, we register a receiver to be notified on time changes. The
- // system only sends the tick out every minute, but that's enough fidelity for our use
- // case
- if (mAutoTimeChangeReceiver == null) {
- mAutoTimeChangeReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DEBUG) {
- Log.d("AutoTimeChangeReceiver", "onReceive | Intent: " + intent);
- }
- dispatchTimeChanged();
- }
- };
- }
- if (mAutoTimeChangeReceiverFilter == null) {
- mAutoTimeChangeReceiverFilter = new IntentFilter();
- mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_CHANGED);
- mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
- mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_TICK);
- }
- mContext.registerReceiver(mAutoTimeChangeReceiver, mAutoTimeChangeReceiverFilter);
- }
-
- final void cleanup() {
- if (mAutoTimeChangeReceiver != null) {
- mContext.unregisterReceiver(mAutoTimeChangeReceiver);
- mAutoTimeChangeReceiver = null;
- }
- }
- }
-}
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV23.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV23.java
index 0ed7409..fd4c781 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV23.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV23.java
@@ -24,7 +24,7 @@
import androidx.annotation.RequiresApi;
@RequiresApi(23)
-class AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 {
+class AppCompatDelegateImplV23 extends AppCompatDelegateImplBase {
private final UiModeManager mUiModeManager;
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV9.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV9.java
deleted file mode 100644
index a0d79ab..0000000
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImplV9.java
+++ /dev/null
@@ -1,2163 +0,0 @@
-/*
- * Copyright (C) 2013 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 androidx.appcompat.app;
-
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static android.view.Window.FEATURE_OPTIONS_PANEL;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.media.AudioManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.util.AndroidRuntimeException;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.FrameLayout;
-import android.widget.PopupWindow;
-import android.widget.TextView;
-
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.appcompat.R;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.appcompat.view.ActionMode;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.appcompat.view.StandaloneActionMode;
-import androidx.appcompat.view.menu.ListMenuPresenter;
-import androidx.appcompat.view.menu.MenuBuilder;
-import androidx.appcompat.view.menu.MenuPresenter;
-import androidx.appcompat.view.menu.MenuView;
-import androidx.appcompat.widget.ActionBarContextView;
-import androidx.appcompat.widget.AppCompatDrawableManager;
-import androidx.appcompat.widget.ContentFrameLayout;
-import androidx.appcompat.widget.DecorContentParent;
-import androidx.appcompat.widget.FitWindowsViewGroup;
-import androidx.appcompat.widget.Toolbar;
-import androidx.appcompat.widget.VectorEnabledTintResources;
-import androidx.appcompat.widget.ViewStubCompat;
-import androidx.appcompat.widget.ViewUtils;
-import androidx.core.app.NavUtils;
-import androidx.core.view.LayoutInflaterCompat;
-import androidx.core.view.OnApplyWindowInsetsListener;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.ViewPropertyAnimatorCompat;
-import androidx.core.view.ViewPropertyAnimatorListenerAdapter;
-import androidx.core.view.WindowCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.core.widget.PopupWindowCompat;
-
-import org.xmlpull.v1.XmlPullParser;
-
-@RequiresApi(14)
-class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
- implements MenuBuilder.Callback, LayoutInflater.Factory2 {
-
- private static final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
-
- private DecorContentParent mDecorContentParent;
- private ActionMenuPresenterCallback mActionMenuPresenterCallback;
- private PanelMenuPresenterCallback mPanelMenuPresenterCallback;
-
- ActionMode mActionMode;
- ActionBarContextView mActionModeView;
- PopupWindow mActionModePopup;
- Runnable mShowActionModePopup;
- ViewPropertyAnimatorCompat mFadeAnim = null;
-
- // true if we have installed a window sub-decor layout.
- private boolean mSubDecorInstalled;
- private ViewGroup mSubDecor;
-
- private TextView mTitleView;
- private View mStatusGuard;
-
- // Used to keep track of Progress Bar Window features
- private boolean mFeatureProgress, mFeatureIndeterminateProgress;
-
- // Used for emulating PanelFeatureState
- private boolean mClosingActionMenu;
- private PanelFeatureState[] mPanels;
- private PanelFeatureState mPreparedPanel;
-
- private boolean mLongPressBackDown;
-
- boolean mInvalidatePanelMenuPosted;
- int mInvalidatePanelMenuFeatures;
- private final Runnable mInvalidatePanelMenuRunnable = new Runnable() {
- @Override
- public void run() {
- if ((mInvalidatePanelMenuFeatures & 1 << FEATURE_OPTIONS_PANEL) != 0) {
- doInvalidatePanelMenu(FEATURE_OPTIONS_PANEL);
- }
- if ((mInvalidatePanelMenuFeatures & 1 << FEATURE_SUPPORT_ACTION_BAR) != 0) {
- doInvalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
- }
- mInvalidatePanelMenuPosted = false;
- mInvalidatePanelMenuFeatures = 0;
- }
- };
-
- private boolean mEnableDefaultActionBarUp;
-
- private Rect mTempRect1;
- private Rect mTempRect2;
-
- private AppCompatViewInflater mAppCompatViewInflater;
-
- AppCompatDelegateImplV9(Context context, Window window, AppCompatCallback callback) {
- super(context, window, callback);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- if (mOriginalWindowCallback instanceof Activity) {
- if (NavUtils.getParentActivityName((Activity) mOriginalWindowCallback) != null) {
- // Peek at the Action Bar and update it if it already exists
- ActionBar ab = peekSupportActionBar();
- if (ab == null) {
- mEnableDefaultActionBarUp = true;
- } else {
- ab.setDefaultDisplayHomeAsUpEnabled(true);
- }
- }
- }
- }
-
- @Override
- public void onPostCreate(Bundle savedInstanceState) {
- // Make sure that the sub decor is installed
- ensureSubDecor();
- }
-
- @Override
- public void initWindowDecorActionBar() {
- ensureSubDecor();
-
- if (!mHasActionBar || mActionBar != null) {
- return;
- }
-
- if (mOriginalWindowCallback instanceof Activity) {
- mActionBar = new WindowDecorActionBar((Activity) mOriginalWindowCallback,
- mOverlayActionBar);
- } else if (mOriginalWindowCallback instanceof Dialog) {
- mActionBar = new WindowDecorActionBar((Dialog) mOriginalWindowCallback);
- }
- if (mActionBar != null) {
- mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
- }
- }
-
- @Override
- public void setSupportActionBar(Toolbar toolbar) {
- if (!(mOriginalWindowCallback instanceof Activity)) {
- // Only Activities support custom Action Bars
- return;
- }
-
- final ActionBar ab = getSupportActionBar();
- if (ab instanceof WindowDecorActionBar) {
- throw new IllegalStateException("This Activity already has an action bar supplied " +
- "by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set " +
- "windowActionBar to false in your theme to use a Toolbar instead.");
- }
-
- // If we reach here then we're setting a new action bar
- // First clear out the MenuInflater to make sure that it is valid for the new Action Bar
- mMenuInflater = null;
-
- // If we have an action bar currently, destroy it
- if (ab != null) {
- ab.onDestroy();
- }
-
- if (toolbar != null) {
- final ToolbarActionBar tbab = new ToolbarActionBar(toolbar,
- ((Activity) mOriginalWindowCallback).getTitle(), mAppCompatWindowCallback);
- mActionBar = tbab;
- mWindow.setCallback(tbab.getWrappedWindowCallback());
- } else {
- mActionBar = null;
- // Re-set the original window callback since we may have already set a Toolbar wrapper
- mWindow.setCallback(mAppCompatWindowCallback);
- }
-
- invalidateOptionsMenu();
- }
-
- @SuppressWarnings("TypeParameterUnusedInFormals")
- @Nullable
- @Override
- public <T extends View> T findViewById(@IdRes int id) {
- ensureSubDecor();
- return (T) mWindow.findViewById(id);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- // If this is called before sub-decor is installed, ActionBar will not
- // be properly initialized.
- if (mHasActionBar && mSubDecorInstalled) {
- // Note: The action bar will need to access
- // view changes from superclass.
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.onConfigurationChanged(newConfig);
- }
- }
-
- // Make sure that the DrawableManager knows about the new config
- AppCompatDrawableManager.get().onConfigurationChanged(mContext);
-
- // Re-apply Day/Night to the new configuration
- applyDayNight();
- }
-
- @Override
- public void onStop() {
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.setShowHideAnimationEnabled(false);
- }
- }
-
- @Override
- public void onPostResume() {
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.setShowHideAnimationEnabled(true);
- }
- }
-
- @Override
- public void setContentView(View v) {
- ensureSubDecor();
- ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
- contentParent.removeAllViews();
- contentParent.addView(v);
- mOriginalWindowCallback.onContentChanged();
- }
-
- @Override
- public void setContentView(int resId) {
- ensureSubDecor();
- ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
- contentParent.removeAllViews();
- LayoutInflater.from(mContext).inflate(resId, contentParent);
- mOriginalWindowCallback.onContentChanged();
- }
-
- @Override
- public void setContentView(View v, ViewGroup.LayoutParams lp) {
- ensureSubDecor();
- ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
- contentParent.removeAllViews();
- contentParent.addView(v, lp);
- mOriginalWindowCallback.onContentChanged();
- }
-
- @Override
- public void addContentView(View v, ViewGroup.LayoutParams lp) {
- ensureSubDecor();
- ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
- contentParent.addView(v, lp);
- mOriginalWindowCallback.onContentChanged();
- }
-
- @Override
- public void onDestroy() {
- if (mInvalidatePanelMenuPosted) {
- mWindow.getDecorView().removeCallbacks(mInvalidatePanelMenuRunnable);
- }
-
- super.onDestroy();
-
- if (mActionBar != null) {
- mActionBar.onDestroy();
- }
- }
-
- private void ensureSubDecor() {
- if (!mSubDecorInstalled) {
- mSubDecor = createSubDecor();
-
- // If a title was set before we installed the decor, propagate it now
- CharSequence title = getTitle();
- if (!TextUtils.isEmpty(title)) {
- onTitleChanged(title);
- }
-
- applyFixedSizeWindow();
-
- onSubDecorInstalled(mSubDecor);
-
- mSubDecorInstalled = true;
-
- // Invalidate if the panel menu hasn't been created before this.
- // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
- // being called in the middle of onCreate or similar.
- // A pending invalidation will typically be resolved before the posted message
- // would run normally in order to satisfy instance state restoration.
- PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
- if (!isDestroyed() && (st == null || st.menu == null)) {
- invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
- }
- }
- }
-
- private ViewGroup createSubDecor() {
- TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
-
- if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
- a.recycle();
- throw new IllegalStateException(
- "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
- }
-
- if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
- requestWindowFeature(Window.FEATURE_NO_TITLE);
- } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
- // Don't allow an action bar if there is no title.
- requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
- }
- if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
- requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
- }
- if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
- requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
- }
- mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
- a.recycle();
-
- // Now let's make sure that the Window has installed its decor by retrieving it
- mWindow.getDecorView();
-
- final LayoutInflater inflater = LayoutInflater.from(mContext);
- ViewGroup subDecor = null;
-
-
- if (!mWindowNoTitle) {
- if (mIsFloating) {
- // If we're floating, inflate the dialog title decor
- subDecor = (ViewGroup) inflater.inflate(
- R.layout.abc_dialog_title_material, null);
-
- // Floating windows can never have an action bar, reset the flags
- mHasActionBar = mOverlayActionBar = false;
- } else if (mHasActionBar) {
- /**
- * This needs some explanation. As we can not use the android:theme attribute
- * pre-L, we emulate it by manually creating a LayoutInflater using a
- * ContextThemeWrapper pointing to actionBarTheme.
- */
- TypedValue outValue = new TypedValue();
- mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
-
- Context themedContext;
- if (outValue.resourceId != 0) {
- themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
- } else {
- themedContext = mContext;
- }
-
- // Now inflate the view using the themed context and set it as the content view
- subDecor = (ViewGroup) LayoutInflater.from(themedContext)
- .inflate(R.layout.abc_screen_toolbar, null);
-
- mDecorContentParent = (DecorContentParent) subDecor
- .findViewById(R.id.decor_content_parent);
- mDecorContentParent.setWindowCallback(getWindowCallback());
-
- /**
- * Propagate features to DecorContentParent
- */
- if (mOverlayActionBar) {
- mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
- }
- if (mFeatureProgress) {
- mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
- }
- if (mFeatureIndeterminateProgress) {
- mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
- }
- }
- } else {
- if (mOverlayActionMode) {
- subDecor = (ViewGroup) inflater.inflate(
- R.layout.abc_screen_simple_overlay_action_mode, null);
- } else {
- subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
- }
-
- if (Build.VERSION.SDK_INT >= 21) {
- // If we're running on L or above, we can rely on ViewCompat's
- // setOnApplyWindowInsetsListener
- ViewCompat.setOnApplyWindowInsetsListener(subDecor,
- new OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- final int top = insets.getSystemWindowInsetTop();
- final int newTop = updateStatusGuard(top);
-
- if (top != newTop) {
- insets = insets.replaceSystemWindowInsets(
- insets.getSystemWindowInsetLeft(),
- newTop,
- insets.getSystemWindowInsetRight(),
- insets.getSystemWindowInsetBottom());
- }
-
- // Now apply the insets on our view
- return ViewCompat.onApplyWindowInsets(v, insets);
- }
- });
- } else {
- // Else, we need to use our own FitWindowsViewGroup handling
- ((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
- new FitWindowsViewGroup.OnFitSystemWindowsListener() {
- @Override
- public void onFitSystemWindows(Rect insets) {
- insets.top = updateStatusGuard(insets.top);
- }
- });
- }
- }
-
- if (subDecor == null) {
- throw new IllegalArgumentException(
- "AppCompat does not support the current theme features: { "
- + "windowActionBar: " + mHasActionBar
- + ", windowActionBarOverlay: "+ mOverlayActionBar
- + ", android:windowIsFloating: " + mIsFloating
- + ", windowActionModeOverlay: " + mOverlayActionMode
- + ", windowNoTitle: " + mWindowNoTitle
- + " }");
- }
-
- if (mDecorContentParent == null) {
- mTitleView = (TextView) subDecor.findViewById(R.id.title);
- }
-
- // Make the decor optionally fit system windows, like the window's decor
- ViewUtils.makeOptionalFitsSystemWindows(subDecor);
-
- final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
- R.id.action_bar_activity_content);
-
- final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
- if (windowContentView != null) {
- // There might be Views already added to the Window's content view so we need to
- // migrate them to our content view
- while (windowContentView.getChildCount() > 0) {
- final View child = windowContentView.getChildAt(0);
- windowContentView.removeViewAt(0);
- contentView.addView(child);
- }
-
- // Change our content FrameLayout to use the android.R.id.content id.
- // Useful for fragments.
- windowContentView.setId(View.NO_ID);
- contentView.setId(android.R.id.content);
-
- // The decorContent may have a foreground drawable set (windowContentOverlay).
- // Remove this as we handle it ourselves
- if (windowContentView instanceof FrameLayout) {
- ((FrameLayout) windowContentView).setForeground(null);
- }
- }
-
- // Now set the Window's content view with the decor
- mWindow.setContentView(subDecor);
-
- contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
- @Override
- public void onAttachedFromWindow() {}
-
- @Override
- public void onDetachedFromWindow() {
- dismissPopups();
- }
- });
-
- return subDecor;
- }
-
- void onSubDecorInstalled(ViewGroup subDecor) {}
-
- private void applyFixedSizeWindow() {
- ContentFrameLayout cfl = (ContentFrameLayout) mSubDecor.findViewById(android.R.id.content);
-
- // This is a bit weird. In the framework, the window sizing attributes control
- // the decor view's size, meaning that any padding is inset for the min/max widths below.
- // We don't control measurement at that level, so we need to workaround it by making sure
- // that the decor view's padding is taken into account.
- final View windowDecor = mWindow.getDecorView();
- cfl.setDecorPadding(windowDecor.getPaddingLeft(),
- windowDecor.getPaddingTop(), windowDecor.getPaddingRight(),
- windowDecor.getPaddingBottom());
-
- TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
- a.getValue(R.styleable.AppCompatTheme_windowMinWidthMajor, cfl.getMinWidthMajor());
- a.getValue(R.styleable.AppCompatTheme_windowMinWidthMinor, cfl.getMinWidthMinor());
-
- if (a.hasValue(R.styleable.AppCompatTheme_windowFixedWidthMajor)) {
- a.getValue(R.styleable.AppCompatTheme_windowFixedWidthMajor,
- cfl.getFixedWidthMajor());
- }
- if (a.hasValue(R.styleable.AppCompatTheme_windowFixedWidthMinor)) {
- a.getValue(R.styleable.AppCompatTheme_windowFixedWidthMinor,
- cfl.getFixedWidthMinor());
- }
- if (a.hasValue(R.styleable.AppCompatTheme_windowFixedHeightMajor)) {
- a.getValue(R.styleable.AppCompatTheme_windowFixedHeightMajor,
- cfl.getFixedHeightMajor());
- }
- if (a.hasValue(R.styleable.AppCompatTheme_windowFixedHeightMinor)) {
- a.getValue(R.styleable.AppCompatTheme_windowFixedHeightMinor,
- cfl.getFixedHeightMinor());
- }
- a.recycle();
-
- cfl.requestLayout();
- }
-
- @Override
- public boolean requestWindowFeature(int featureId) {
- featureId = sanitizeWindowFeatureId(featureId);
-
- if (mWindowNoTitle && featureId == FEATURE_SUPPORT_ACTION_BAR) {
- return false; // Ignore. No title dominates.
- }
- if (mHasActionBar && featureId == Window.FEATURE_NO_TITLE) {
- // Remove the action bar feature if we have no title. No title dominates.
- mHasActionBar = false;
- }
-
- switch (featureId) {
- case FEATURE_SUPPORT_ACTION_BAR:
- throwFeatureRequestIfSubDecorInstalled();
- mHasActionBar = true;
- return true;
- case FEATURE_SUPPORT_ACTION_BAR_OVERLAY:
- throwFeatureRequestIfSubDecorInstalled();
- mOverlayActionBar = true;
- return true;
- case FEATURE_ACTION_MODE_OVERLAY:
- throwFeatureRequestIfSubDecorInstalled();
- mOverlayActionMode = true;
- return true;
- case Window.FEATURE_PROGRESS:
- throwFeatureRequestIfSubDecorInstalled();
- mFeatureProgress = true;
- return true;
- case Window.FEATURE_INDETERMINATE_PROGRESS:
- throwFeatureRequestIfSubDecorInstalled();
- mFeatureIndeterminateProgress = true;
- return true;
- case Window.FEATURE_NO_TITLE:
- throwFeatureRequestIfSubDecorInstalled();
- mWindowNoTitle = true;
- return true;
- }
-
- return mWindow.requestFeature(featureId);
- }
-
- @Override
- public boolean hasWindowFeature(int featureId) {
- featureId = sanitizeWindowFeatureId(featureId);
- switch (featureId) {
- case FEATURE_SUPPORT_ACTION_BAR:
- return mHasActionBar;
- case FEATURE_SUPPORT_ACTION_BAR_OVERLAY:
- return mOverlayActionBar;
- case FEATURE_ACTION_MODE_OVERLAY:
- return mOverlayActionMode;
- case Window.FEATURE_PROGRESS:
- return mFeatureProgress;
- case Window.FEATURE_INDETERMINATE_PROGRESS:
- return mFeatureIndeterminateProgress;
- case Window.FEATURE_NO_TITLE:
- return mWindowNoTitle;
- }
- return false;
- }
-
- @Override
- void onTitleChanged(CharSequence title) {
- if (mDecorContentParent != null) {
- mDecorContentParent.setWindowTitle(title);
- } else if (peekSupportActionBar() != null) {
- peekSupportActionBar().setWindowTitle(title);
- } else if (mTitleView != null) {
- mTitleView.setText(title);
- }
- }
-
- @Override
- void onPanelClosed(final int featureId, Menu menu) {
- if (featureId == FEATURE_SUPPORT_ACTION_BAR) {
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.dispatchMenuVisibilityChanged(false);
- }
- } else if (featureId == FEATURE_OPTIONS_PANEL) {
- // Make sure that the options panel is closed. This is mainly used when we're using a
- // ToolbarActionBar
- PanelFeatureState st = getPanelState(featureId, true);
- if (st.isOpen) {
- closePanel(st, false);
- }
- }
- }
-
- @Override
- boolean onMenuOpened(final int featureId, Menu menu) {
- if (featureId == FEATURE_SUPPORT_ACTION_BAR) {
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.dispatchMenuVisibilityChanged(true);
- }
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
- final Window.Callback cb = getWindowCallback();
- if (cb != null && !isDestroyed()) {
- final PanelFeatureState panel = findMenuPanel(menu.getRootMenu());
- if (panel != null) {
- return cb.onMenuItemSelected(panel.featureId, item);
- }
- }
- return false;
- }
-
- @Override
- public void onMenuModeChange(MenuBuilder menu) {
- reopenMenu(menu, true);
- }
-
- @Override
- public ActionMode startSupportActionMode(@NonNull final ActionMode.Callback callback) {
- if (callback == null) {
- throw new IllegalArgumentException("ActionMode callback can not be null.");
- }
-
- if (mActionMode != null) {
- mActionMode.finish();
- }
-
- final ActionMode.Callback wrappedCallback = new ActionModeCallbackWrapperV9(callback);
-
- ActionBar ab = getSupportActionBar();
- if (ab != null) {
- mActionMode = ab.startActionMode(wrappedCallback);
- if (mActionMode != null && mAppCompatCallback != null) {
- mAppCompatCallback.onSupportActionModeStarted(mActionMode);
- }
- }
-
- if (mActionMode == null) {
- // If the action bar didn't provide an action mode, start the emulated window one
- mActionMode = startSupportActionModeFromWindow(wrappedCallback);
- }
-
- return mActionMode;
- }
-
- @Override
- public void invalidateOptionsMenu() {
- final ActionBar ab = getSupportActionBar();
- if (ab != null && ab.invalidateOptionsMenu()) return;
-
- invalidatePanelMenu(FEATURE_OPTIONS_PANEL);
- }
-
- @Override
- ActionMode startSupportActionModeFromWindow(@NonNull ActionMode.Callback callback) {
- endOnGoingFadeAnimation();
- if (mActionMode != null) {
- mActionMode.finish();
- }
-
- if (!(callback instanceof ActionModeCallbackWrapperV9)) {
- // If the callback hasn't been wrapped yet, wrap it
- callback = new ActionModeCallbackWrapperV9(callback);
- }
-
- ActionMode mode = null;
- if (mAppCompatCallback != null && !isDestroyed()) {
- try {
- mode = mAppCompatCallback.onWindowStartingSupportActionMode(callback);
- } catch (AbstractMethodError ame) {
- // Older apps might not implement this callback method.
- }
- }
-
- if (mode != null) {
- mActionMode = mode;
- } else {
- if (mActionModeView == null) {
- if (mIsFloating) {
- // Use the action bar theme.
- final TypedValue outValue = new TypedValue();
- final Resources.Theme baseTheme = mContext.getTheme();
- baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true);
-
- final Context actionBarContext;
- if (outValue.resourceId != 0) {
- final Resources.Theme actionBarTheme = mContext.getResources().newTheme();
- actionBarTheme.setTo(baseTheme);
- actionBarTheme.applyStyle(outValue.resourceId, true);
-
- actionBarContext = new ContextThemeWrapper(mContext, 0);
- actionBarContext.getTheme().setTo(actionBarTheme);
- } else {
- actionBarContext = mContext;
- }
-
- mActionModeView = new ActionBarContextView(actionBarContext);
- mActionModePopup = new PopupWindow(actionBarContext, null,
- R.attr.actionModePopupWindowStyle);
- PopupWindowCompat.setWindowLayoutType(mActionModePopup,
- WindowManager.LayoutParams.TYPE_APPLICATION);
- mActionModePopup.setContentView(mActionModeView);
- mActionModePopup.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
-
- actionBarContext.getTheme().resolveAttribute(
- R.attr.actionBarSize, outValue, true);
- final int height = TypedValue.complexToDimensionPixelSize(outValue.data,
- actionBarContext.getResources().getDisplayMetrics());
- mActionModeView.setContentHeight(height);
- mActionModePopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
- mShowActionModePopup = new Runnable() {
- @Override
- public void run() {
- mActionModePopup.showAtLocation(
- mActionModeView,
- Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0);
- endOnGoingFadeAnimation();
-
- if (shouldAnimateActionModeView()) {
- mActionModeView.setAlpha(0f);
- mFadeAnim = ViewCompat.animate(mActionModeView).alpha(1f);
- mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(View view) {
- mActionModeView.setVisibility(View.VISIBLE);
- }
-
- @Override
- public void onAnimationEnd(View view) {
- mActionModeView.setAlpha(1f);
- mFadeAnim.setListener(null);
- mFadeAnim = null;
- }
- });
- } else {
- mActionModeView.setAlpha(1f);
- mActionModeView.setVisibility(View.VISIBLE);
- }
- }
- };
- } else {
- ViewStubCompat stub = (ViewStubCompat) mSubDecor
- .findViewById(R.id.action_mode_bar_stub);
- if (stub != null) {
- // Set the layout inflater so that it is inflated with the action bar's context
- stub.setLayoutInflater(LayoutInflater.from(getActionBarThemedContext()));
- mActionModeView = (ActionBarContextView) stub.inflate();
- }
- }
- }
-
- if (mActionModeView != null) {
- endOnGoingFadeAnimation();
- mActionModeView.killMode();
- mode = new StandaloneActionMode(mActionModeView.getContext(), mActionModeView,
- callback, mActionModePopup == null);
- if (callback.onCreateActionMode(mode, mode.getMenu())) {
- mode.invalidate();
- mActionModeView.initForMode(mode);
- mActionMode = mode;
-
- if (shouldAnimateActionModeView()) {
- mActionModeView.setAlpha(0f);
- mFadeAnim = ViewCompat.animate(mActionModeView).alpha(1f);
- mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(View view) {
- mActionModeView.setVisibility(View.VISIBLE);
- mActionModeView.sendAccessibilityEvent(
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
- if (mActionModeView.getParent() instanceof View) {
- ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
- }
- }
-
- @Override
- public void onAnimationEnd(View view) {
- mActionModeView.setAlpha(1f);
- mFadeAnim.setListener(null);
- mFadeAnim = null;
- }
- });
- } else {
- mActionModeView.setAlpha(1f);
- mActionModeView.setVisibility(View.VISIBLE);
- mActionModeView.sendAccessibilityEvent(
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
- if (mActionModeView.getParent() instanceof View) {
- ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
- }
- }
-
- if (mActionModePopup != null) {
- mWindow.getDecorView().post(mShowActionModePopup);
- }
- } else {
- mActionMode = null;
- }
- }
- }
- if (mActionMode != null && mAppCompatCallback != null) {
- mAppCompatCallback.onSupportActionModeStarted(mActionMode);
- }
- return mActionMode;
- }
-
- final boolean shouldAnimateActionModeView() {
- // We only to animate the action mode in if the sub decor has already been laid out.
- // If it hasn't been laid out, it hasn't been drawn to screen yet.
- return mSubDecorInstalled && mSubDecor != null && ViewCompat.isLaidOut(mSubDecor);
- }
-
- void endOnGoingFadeAnimation() {
- if (mFadeAnim != null) {
- mFadeAnim.cancel();
- }
- }
-
- boolean onBackPressed() {
- // Back cancels action modes first.
- if (mActionMode != null) {
- mActionMode.finish();
- return true;
- }
-
- // Next collapse any expanded action views.
- ActionBar ab = getSupportActionBar();
- if (ab != null && ab.collapseActionView()) {
- return true;
- }
-
- // Let the call through...
- return false;
- }
-
- @Override
- boolean onKeyShortcut(int keyCode, KeyEvent ev) {
- // Let the Action Bar have a chance at handling the shortcut
- ActionBar ab = getSupportActionBar();
- if (ab != null && ab.onKeyShortcut(keyCode, ev)) {
- return true;
- }
-
- // If the panel is already prepared, then perform the shortcut using it.
- boolean handled;
- if (mPreparedPanel != null) {
- handled = performPanelShortcut(mPreparedPanel, ev.getKeyCode(), ev,
- Menu.FLAG_PERFORM_NO_CLOSE);
- if (handled) {
- if (mPreparedPanel != null) {
- mPreparedPanel.isHandled = true;
- }
- return true;
- }
- }
-
- // If the panel is not prepared, then we may be trying to handle a shortcut key
- // combination such as Control+C. Temporarily prepare the panel then mark it
- // unprepared again when finished to ensure that the panel will again be prepared
- // the next time it is shown for real.
- if (mPreparedPanel == null) {
- PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
- preparePanel(st, ev);
- handled = performPanelShortcut(st, ev.getKeyCode(), ev, Menu.FLAG_PERFORM_NO_CLOSE);
- st.isPrepared = false;
- if (handled) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
- // If this is a MENU event, let the Activity have a go.
- if (mOriginalWindowCallback.dispatchKeyEvent(event)) {
- return true;
- }
- }
-
- final int keyCode = event.getKeyCode();
- final int action = event.getAction();
- final boolean isDown = action == KeyEvent.ACTION_DOWN;
-
- return isDown ? onKeyDown(keyCode, event) : onKeyUp(keyCode, event);
- }
-
- boolean onKeyUp(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_MENU:
- onKeyUpPanel(Window.FEATURE_OPTIONS_PANEL, event);
- return true;
- case KeyEvent.KEYCODE_BACK:
- final boolean wasLongPressBackDown = mLongPressBackDown;
- mLongPressBackDown = false;
-
- PanelFeatureState st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
- if (st != null && st.isOpen) {
- if (!wasLongPressBackDown) {
- // Certain devices allow opening the options menu via a long press of the
- // back button. We should only close the open options menu if it wasn't
- // opened via a long press gesture.
- closePanel(st, true);
- }
- return true;
- }
- if (onBackPressed()) {
- return true;
- }
- break;
- }
- return false;
- }
-
- boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_MENU:
- onKeyDownPanel(Window.FEATURE_OPTIONS_PANEL, event);
- // We need to return true here and not let it bubble up to the Window.
- // For empty menus, PhoneWindow's KEYCODE_BACK handling will steals all events,
- // not allowing the Activity to call onBackPressed().
- return true;
- case KeyEvent.KEYCODE_BACK:
- // Certain devices allow opening the options menu via a long press of the back
- // button. We keep a record of whether the last event is from a long press.
- mLongPressBackDown = (event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
- break;
- }
- return false;
- }
-
- @Override
- public View createView(View parent, final String name, @NonNull Context context,
- @NonNull AttributeSet attrs) {
- if (mAppCompatViewInflater == null) {
- TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
- String viewInflaterClassName =
- a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
- if ((viewInflaterClassName == null)
- || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
- // Either default class name or set explicitly to null. In both cases
- // create the base inflater (no reflection)
- mAppCompatViewInflater = new AppCompatViewInflater();
- } else {
- try {
- Class viewInflaterClass = Class.forName(viewInflaterClassName);
- mAppCompatViewInflater =
- (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
- .newInstance();
- } catch (Throwable t) {
- Log.i(TAG, "Failed to instantiate custom view inflater "
- + viewInflaterClassName + ". Falling back to default.", t);
- mAppCompatViewInflater = new AppCompatViewInflater();
- }
- }
- }
-
- boolean inheritContext = false;
- if (IS_PRE_LOLLIPOP) {
- inheritContext = (attrs instanceof XmlPullParser)
- // If we have a XmlPullParser, we can detect where we are in the layout
- ? ((XmlPullParser) attrs).getDepth() > 1
- // Otherwise we have to use the old heuristic
- : shouldInheritContext((ViewParent) parent);
- }
-
- return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
- IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
- true, /* Read read app:theme as a fallback at all times for legacy reasons */
- VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
- );
- }
-
- private boolean shouldInheritContext(ViewParent parent) {
- if (parent == null) {
- // The initial parent is null so just return false
- return false;
- }
- final View windowDecor = mWindow.getDecorView();
- while (true) {
- if (parent == null) {
- // Bingo. We've hit a view which has a null parent before being terminated from
- // the loop. This is (most probably) because it's the root view in an inflation
- // call, therefore we should inherit. This works as the inflated layout is only
- // added to the hierarchy at the end of the inflate() call.
- return true;
- } else if (parent == windowDecor || !(parent instanceof View)
- || ViewCompat.isAttachedToWindow((View) parent)) {
- // We have either hit the window's decor view, a parent which isn't a View
- // (i.e. ViewRootImpl), or an attached view, so we know that the original parent
- // is currently added to the view hierarchy. This means that it has not be
- // inflated in the current inflate() call and we should not inherit the context.
- return false;
- }
- parent = parent.getParent();
- }
- }
-
- @Override
- public void installViewFactory() {
- LayoutInflater layoutInflater = LayoutInflater.from(mContext);
- if (layoutInflater.getFactory() == null) {
- LayoutInflaterCompat.setFactory2(layoutInflater, this);
- } else {
- if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
- Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
- + " so we can not install AppCompat's");
- }
- }
- }
-
- /**
- * From {@link LayoutInflater.Factory2}.
- */
- @Override
- public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
- // First let the Activity's Factory try and inflate the view
- final View view = callActivityOnCreateView(parent, name, context, attrs);
- if (view != null) {
- return view;
- }
-
- // If the Factory didn't handle it, let our createView() method try
- return createView(parent, name, context, attrs);
- }
-
- /**
- * From {@link LayoutInflater.Factory2}.
- */
- @Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
- return onCreateView(null, name, context, attrs);
- }
-
- View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
- // Let the Activity's LayoutInflater.Factory try and handle it
- if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
- final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
- .onCreateView(name, context, attrs);
- if (result != null) {
- return result;
- }
- }
- return null;
- }
-
- private void openPanel(final PanelFeatureState st, KeyEvent event) {
- // Already open, return
- if (st.isOpen || isDestroyed()) {
- return;
- }
-
- // Don't open an options panel on xlarge devices.
- // (The app should be using an action bar for menu items.)
- if (st.featureId == FEATURE_OPTIONS_PANEL) {
- Configuration config = mContext.getResources().getConfiguration();
- boolean isXLarge = (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
- == Configuration.SCREENLAYOUT_SIZE_XLARGE;
- if (isXLarge) {
- return;
- }
- }
-
- Window.Callback cb = getWindowCallback();
- if ((cb != null) && (!cb.onMenuOpened(st.featureId, st.menu))) {
- // Callback doesn't want the menu to open, reset any state
- closePanel(st, true);
- return;
- }
-
- final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
- if (wm == null) {
- return;
- }
-
- // Prepare panel (should have been done before, but just in case)
- if (!preparePanel(st, event)) {
- return;
- }
-
- int width = WRAP_CONTENT;
- if (st.decorView == null || st.refreshDecorView) {
- if (st.decorView == null) {
- // Initialize the panel decor, this will populate st.decorView
- if (!initializePanelDecor(st) || (st.decorView == null))
- return;
- } else if (st.refreshDecorView && (st.decorView.getChildCount() > 0)) {
- // Decor needs refreshing, so remove its views
- st.decorView.removeAllViews();
- }
-
- // This will populate st.shownPanelView
- if (!initializePanelContent(st) || !st.hasPanelItems()) {
- return;
- }
-
- ViewGroup.LayoutParams lp = st.shownPanelView.getLayoutParams();
- if (lp == null) {
- lp = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
- }
-
- int backgroundResId = st.background;
- st.decorView.setBackgroundResource(backgroundResId);
-
- ViewParent shownPanelParent = st.shownPanelView.getParent();
- if (shownPanelParent != null && shownPanelParent instanceof ViewGroup) {
- ((ViewGroup) shownPanelParent).removeView(st.shownPanelView);
- }
- st.decorView.addView(st.shownPanelView, lp);
-
- /*
- * Give focus to the view, if it or one of its children does not
- * already have it.
- */
- if (!st.shownPanelView.hasFocus()) {
- st.shownPanelView.requestFocus();
- }
- } else if (st.createdPanelView != null) {
- // If we already had a panel view, carry width=MATCH_PARENT through
- // as we did above when it was created.
- ViewGroup.LayoutParams lp = st.createdPanelView.getLayoutParams();
- if (lp != null && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
- width = MATCH_PARENT;
- }
- }
-
- st.isHandled = false;
-
- WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
- width, WRAP_CONTENT,
- st.x, st.y, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
- WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
- | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
- PixelFormat.TRANSLUCENT);
-
- lp.gravity = st.gravity;
- lp.windowAnimations = st.windowAnimations;
-
- wm.addView(st.decorView, lp);
- st.isOpen = true;
- }
-
- private boolean initializePanelDecor(PanelFeatureState st) {
- st.setStyle(getActionBarThemedContext());
- st.decorView = new ListMenuDecorView(st.listPresenterContext);
- st.gravity = Gravity.CENTER | Gravity.BOTTOM;
- return true;
- }
-
- private void reopenMenu(MenuBuilder menu, boolean toggleMenuMode) {
- if (mDecorContentParent != null && mDecorContentParent.canShowOverflowMenu()
- && (!ViewConfiguration.get(mContext).hasPermanentMenuKey()
- || mDecorContentParent.isOverflowMenuShowPending())) {
-
- final Window.Callback cb = getWindowCallback();
-
- if (!mDecorContentParent.isOverflowMenuShowing() || !toggleMenuMode) {
- if (cb != null && !isDestroyed()) {
- // If we have a menu invalidation pending, do it now.
- if (mInvalidatePanelMenuPosted &&
- (mInvalidatePanelMenuFeatures & (1 << FEATURE_OPTIONS_PANEL)) != 0) {
- mWindow.getDecorView().removeCallbacks(mInvalidatePanelMenuRunnable);
- mInvalidatePanelMenuRunnable.run();
- }
-
- final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
-
- // If we don't have a menu or we're waiting for a full content refresh,
- // forget it. This is a lingering event that no longer matters.
- if (st.menu != null && !st.refreshMenuContent &&
- cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) {
- cb.onMenuOpened(FEATURE_SUPPORT_ACTION_BAR, st.menu);
- mDecorContentParent.showOverflowMenu();
- }
- }
- } else {
- mDecorContentParent.hideOverflowMenu();
- if (!isDestroyed()) {
- final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
- cb.onPanelClosed(FEATURE_SUPPORT_ACTION_BAR, st.menu);
- }
- }
- return;
- }
-
- PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true);
-
- st.refreshDecorView = true;
- closePanel(st, false);
-
- openPanel(st, null);
- }
-
- private boolean initializePanelMenu(final PanelFeatureState st) {
- Context context = mContext;
-
- // If we have an action bar, initialize the menu with the right theme.
- if ((st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_SUPPORT_ACTION_BAR) &&
- mDecorContentParent != null) {
- final TypedValue outValue = new TypedValue();
- final Resources.Theme baseTheme = context.getTheme();
- baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true);
-
- Resources.Theme widgetTheme = null;
- if (outValue.resourceId != 0) {
- widgetTheme = context.getResources().newTheme();
- widgetTheme.setTo(baseTheme);
- widgetTheme.applyStyle(outValue.resourceId, true);
- widgetTheme.resolveAttribute(
- R.attr.actionBarWidgetTheme, outValue, true);
- } else {
- baseTheme.resolveAttribute(
- R.attr.actionBarWidgetTheme, outValue, true);
- }
-
- if (outValue.resourceId != 0) {
- if (widgetTheme == null) {
- widgetTheme = context.getResources().newTheme();
- widgetTheme.setTo(baseTheme);
- }
- widgetTheme.applyStyle(outValue.resourceId, true);
- }
-
- if (widgetTheme != null) {
- context = new ContextThemeWrapper(context, 0);
- context.getTheme().setTo(widgetTheme);
- }
- }
-
- final MenuBuilder menu = new MenuBuilder(context);
- menu.setCallback(this);
- st.setMenu(menu);
-
- return true;
- }
-
- private boolean initializePanelContent(PanelFeatureState st) {
- if (st.createdPanelView != null) {
- st.shownPanelView = st.createdPanelView;
- return true;
- }
-
- if (st.menu == null) {
- return false;
- }
-
- if (mPanelMenuPresenterCallback == null) {
- mPanelMenuPresenterCallback = new PanelMenuPresenterCallback();
- }
-
- MenuView menuView = st.getListMenuView(mPanelMenuPresenterCallback);
-
- st.shownPanelView = (View) menuView;
-
- return st.shownPanelView != null;
- }
-
- private boolean preparePanel(PanelFeatureState st, KeyEvent event) {
- if (isDestroyed()) {
- return false;
- }
-
- // Already prepared (isPrepared will be reset to false later)
- if (st.isPrepared) {
- return true;
- }
-
- if ((mPreparedPanel != null) && (mPreparedPanel != st)) {
- // Another Panel is prepared and possibly open, so close it
- closePanel(mPreparedPanel, false);
- }
-
- final Window.Callback cb = getWindowCallback();
-
- if (cb != null) {
- st.createdPanelView = cb.onCreatePanelView(st.featureId);
- }
-
- final boolean isActionBarMenu =
- (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_SUPPORT_ACTION_BAR);
-
- if (isActionBarMenu && mDecorContentParent != null) {
- // Enforce ordering guarantees around events so that the action bar never
- // dispatches menu-related events before the panel is prepared.
- mDecorContentParent.setMenuPrepared();
- }
-
- if (st.createdPanelView == null &&
- (!isActionBarMenu || !(peekSupportActionBar() instanceof ToolbarActionBar))) {
- // Since ToolbarActionBar handles the list options menu itself, we only want to
- // init this menu panel if we're not using a TAB.
- if (st.menu == null || st.refreshMenuContent) {
- if (st.menu == null) {
- if (!initializePanelMenu(st) || (st.menu == null)) {
- return false;
- }
- }
-
- if (isActionBarMenu && mDecorContentParent != null) {
- if (mActionMenuPresenterCallback == null) {
- mActionMenuPresenterCallback = new ActionMenuPresenterCallback();
- }
- mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback);
- }
-
- // Creating the panel menu will involve a lot of manipulation;
- // don't dispatch change events to presenters until we're done.
- st.menu.stopDispatchingItemsChanged();
- if (!cb.onCreatePanelMenu(st.featureId, st.menu)) {
- // Ditch the menu created above
- st.setMenu(null);
-
- if (isActionBarMenu && mDecorContentParent != null) {
- // Don't show it in the action bar either
- mDecorContentParent.setMenu(null, mActionMenuPresenterCallback);
- }
-
- return false;
- }
-
- st.refreshMenuContent = false;
- }
-
- // Preparing the panel menu can involve a lot of manipulation;
- // don't dispatch change events to presenters until we're done.
- st.menu.stopDispatchingItemsChanged();
-
- // Restore action view state before we prepare. This gives apps
- // an opportunity to override frozen/restored state in onPrepare.
- if (st.frozenActionViewState != null) {
- st.menu.restoreActionViewStates(st.frozenActionViewState);
- st.frozenActionViewState = null;
- }
-
- // Callback and return if the callback does not want to show the menu
- if (!cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) {
- if (isActionBarMenu && mDecorContentParent != null) {
- // The app didn't want to show the menu for now but it still exists.
- // Clear it out of the action bar.
- mDecorContentParent.setMenu(null, mActionMenuPresenterCallback);
- }
- st.menu.startDispatchingItemsChanged();
- return false;
- }
-
- // Set the proper keymap
- KeyCharacterMap kmap = KeyCharacterMap.load(
- event != null ? event.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD);
- st.qwertyMode = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC;
- st.menu.setQwertyMode(st.qwertyMode);
- st.menu.startDispatchingItemsChanged();
- }
-
- // Set other state
- st.isPrepared = true;
- st.isHandled = false;
- mPreparedPanel = st;
-
- return true;
- }
-
- void checkCloseActionMenu(MenuBuilder menu) {
- if (mClosingActionMenu) {
- return;
- }
-
- mClosingActionMenu = true;
- mDecorContentParent.dismissPopups();
- Window.Callback cb = getWindowCallback();
- if (cb != null && !isDestroyed()) {
- cb.onPanelClosed(FEATURE_SUPPORT_ACTION_BAR, menu);
- }
- mClosingActionMenu = false;
- }
-
- void closePanel(int featureId) {
- closePanel(getPanelState(featureId, true), true);
- }
-
- void closePanel(PanelFeatureState st, boolean doCallback) {
- if (doCallback && st.featureId == FEATURE_OPTIONS_PANEL &&
- mDecorContentParent != null && mDecorContentParent.isOverflowMenuShowing()) {
- checkCloseActionMenu(st.menu);
- return;
- }
-
- final WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
- if (wm != null && st.isOpen && st.decorView != null) {
- wm.removeView(st.decorView);
-
- if (doCallback) {
- callOnPanelClosed(st.featureId, st, null);
- }
- }
-
- st.isPrepared = false;
- st.isHandled = false;
- st.isOpen = false;
-
- // This view is no longer shown, so null it out
- st.shownPanelView = null;
-
- // Next time the menu opens, it should not be in expanded mode, so
- // force a refresh of the decor
- st.refreshDecorView = true;
-
- if (mPreparedPanel == st) {
- mPreparedPanel = null;
- }
- }
-
- private boolean onKeyDownPanel(int featureId, KeyEvent event) {
- if (event.getRepeatCount() == 0) {
- PanelFeatureState st = getPanelState(featureId, true);
- if (!st.isOpen) {
- return preparePanel(st, event);
- }
- }
-
- return false;
- }
-
- private boolean onKeyUpPanel(int featureId, KeyEvent event) {
- if (mActionMode != null) {
- return false;
- }
-
- boolean handled = false;
- final PanelFeatureState st = getPanelState(featureId, true);
- if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null &&
- mDecorContentParent.canShowOverflowMenu() &&
- !ViewConfiguration.get(mContext).hasPermanentMenuKey()) {
- if (!mDecorContentParent.isOverflowMenuShowing()) {
- if (!isDestroyed() && preparePanel(st, event)) {
- handled = mDecorContentParent.showOverflowMenu();
- }
- } else {
- handled = mDecorContentParent.hideOverflowMenu();
- }
- } else {
- if (st.isOpen || st.isHandled) {
- // Play the sound effect if the user closed an open menu (and not if
- // they just released a menu shortcut)
- handled = st.isOpen;
- // Close menu
- closePanel(st, true);
- } else if (st.isPrepared) {
- boolean show = true;
- if (st.refreshMenuContent) {
- // Something may have invalidated the menu since we prepared it.
- // Re-prepare it to refresh.
- st.isPrepared = false;
- show = preparePanel(st, event);
- }
-
- if (show) {
- // Show menu
- openPanel(st, event);
- handled = true;
- }
- }
- }
-
- if (handled) {
- AudioManager audioManager = (AudioManager) mContext.getSystemService(
- Context.AUDIO_SERVICE);
- if (audioManager != null) {
- audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
- } else {
- Log.w(TAG, "Couldn't get audio manager");
- }
- }
- return handled;
- }
-
- void callOnPanelClosed(int featureId, PanelFeatureState panel, Menu menu) {
- // Try to get a menu
- if (menu == null) {
- // Need a panel to grab the menu, so try to get that
- if (panel == null) {
- if ((featureId >= 0) && (featureId < mPanels.length)) {
- panel = mPanels[featureId];
- }
- }
-
- if (panel != null) {
- // menu still may be null, which is okay--we tried our best
- menu = panel.menu;
- }
- }
-
- // If the panel is not open, do not callback
- if ((panel != null) && (!panel.isOpen))
- return;
-
- if (!isDestroyed()) {
- // We need to be careful which callback we dispatch the call to. We can not dispatch
- // this to the Window's callback since that will call back into this method and cause a
- // crash. Instead we need to dispatch down to the original Activity/Dialog/etc.
- mOriginalWindowCallback.onPanelClosed(featureId, menu);
- }
- }
-
- PanelFeatureState findMenuPanel(Menu menu) {
- final PanelFeatureState[] panels = mPanels;
- final int N = panels != null ? panels.length : 0;
- for (int i = 0; i < N; i++) {
- final PanelFeatureState panel = panels[i];
- if (panel != null && panel.menu == menu) {
- return panel;
- }
- }
- return null;
- }
-
- protected PanelFeatureState getPanelState(int featureId, boolean required) {
- PanelFeatureState[] ar;
- if ((ar = mPanels) == null || ar.length <= featureId) {
- PanelFeatureState[] nar = new PanelFeatureState[featureId + 1];
- if (ar != null) {
- System.arraycopy(ar, 0, nar, 0, ar.length);
- }
- mPanels = ar = nar;
- }
-
- PanelFeatureState st = ar[featureId];
- if (st == null) {
- ar[featureId] = st = new PanelFeatureState(featureId);
- }
- return st;
- }
-
- private boolean performPanelShortcut(PanelFeatureState st, int keyCode, KeyEvent event,
- int flags) {
- if (event.isSystem()) {
- return false;
- }
-
- boolean handled = false;
-
- // Only try to perform menu shortcuts if preparePanel returned true (possible false
- // return value from application not wanting to show the menu).
- if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) {
- // The menu is prepared now, perform the shortcut on it
- handled = st.menu.performShortcut(keyCode, event, flags);
- }
-
- if (handled) {
- // Only close down the menu if we don't have an action bar keeping it open.
- if ((flags & Menu.FLAG_PERFORM_NO_CLOSE) == 0 && mDecorContentParent == null) {
- closePanel(st, true);
- }
- }
-
- return handled;
- }
-
- private void invalidatePanelMenu(int featureId) {
- mInvalidatePanelMenuFeatures |= 1 << featureId;
-
- if (!mInvalidatePanelMenuPosted) {
- ViewCompat.postOnAnimation(mWindow.getDecorView(), mInvalidatePanelMenuRunnable);
- mInvalidatePanelMenuPosted = true;
- }
- }
-
- void doInvalidatePanelMenu(int featureId) {
- PanelFeatureState st = getPanelState(featureId, true);
- Bundle savedActionViewStates = null;
- if (st.menu != null) {
- savedActionViewStates = new Bundle();
- st.menu.saveActionViewStates(savedActionViewStates);
- if (savedActionViewStates.size() > 0) {
- st.frozenActionViewState = savedActionViewStates;
- }
- // This will be started again when the panel is prepared.
- st.menu.stopDispatchingItemsChanged();
- st.menu.clear();
- }
- st.refreshMenuContent = true;
- st.refreshDecorView = true;
-
- // Prepare the options panel if we have an action bar
- if ((featureId == FEATURE_SUPPORT_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL)
- && mDecorContentParent != null) {
- st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
- if (st != null) {
- st.isPrepared = false;
- preparePanel(st, null);
- }
- }
- }
-
- /**
- * Updates the status bar guard
- *
- * @param insetTop the current top system window inset
- * @return the new top system window inset
- */
- int updateStatusGuard(int insetTop) {
- boolean showStatusGuard = false;
- // Show the status guard when the non-overlay contextual action bar is showing
- if (mActionModeView != null) {
- if (mActionModeView.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
- ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)
- mActionModeView.getLayoutParams();
- boolean mlpChanged = false;
-
- if (mActionModeView.isShown()) {
- if (mTempRect1 == null) {
- mTempRect1 = new Rect();
- mTempRect2 = new Rect();
- }
- final Rect insets = mTempRect1;
- final Rect localInsets = mTempRect2;
- insets.set(0, insetTop, 0, 0);
-
- ViewUtils.computeFitSystemWindows(mSubDecor, insets, localInsets);
- final int newMargin = localInsets.top == 0 ? insetTop : 0;
- if (mlp.topMargin != newMargin) {
- mlpChanged = true;
- mlp.topMargin = insetTop;
-
- if (mStatusGuard == null) {
- mStatusGuard = new View(mContext);
- mStatusGuard.setBackgroundColor(mContext.getResources()
- .getColor(R.color.abc_input_method_navigation_guard));
- mSubDecor.addView(mStatusGuard, -1,
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- insetTop));
- } else {
- ViewGroup.LayoutParams lp = mStatusGuard.getLayoutParams();
- if (lp.height != insetTop) {
- lp.height = insetTop;
- mStatusGuard.setLayoutParams(lp);
- }
- }
- }
-
- // The action mode's theme may differ from the app, so
- // always show the status guard above it.
- showStatusGuard = mStatusGuard != null;
-
- // We only need to consume the insets if the action
- // mode is overlaid on the app content (e.g. it's
- // sitting in a FrameLayout, see
- // screen_simple_overlay_action_mode.xml).
- if (!mOverlayActionMode && showStatusGuard) {
- insetTop = 0;
- }
- } else {
- // reset top margin
- if (mlp.topMargin != 0) {
- mlpChanged = true;
- mlp.topMargin = 0;
- }
- }
- if (mlpChanged) {
- mActionModeView.setLayoutParams(mlp);
- }
- }
- }
- if (mStatusGuard != null) {
- mStatusGuard.setVisibility(showStatusGuard ? View.VISIBLE : View.GONE);
- }
-
- return insetTop;
- }
-
- private void throwFeatureRequestIfSubDecorInstalled() {
- if (mSubDecorInstalled) {
- throw new AndroidRuntimeException(
- "Window feature must be requested before adding content");
- }
- }
-
- private int sanitizeWindowFeatureId(int featureId) {
- if (featureId == WindowCompat.FEATURE_ACTION_BAR) {
- Log.i(TAG, "You should now use the AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR"
- + " id when requesting this feature.");
- return FEATURE_SUPPORT_ACTION_BAR;
- } else if (featureId == WindowCompat.FEATURE_ACTION_BAR_OVERLAY) {
- Log.i(TAG, "You should now use the AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR_OVERLAY"
- + " id when requesting this feature.");
- return FEATURE_SUPPORT_ACTION_BAR_OVERLAY;
- }
- // Else we'll just return the original id
- return featureId;
- }
-
- ViewGroup getSubDecor() {
- return mSubDecor;
- }
-
- void dismissPopups() {
- if (mDecorContentParent != null) {
- mDecorContentParent.dismissPopups();
- }
-
- if (mActionModePopup != null) {
- mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
- if (mActionModePopup.isShowing()) {
- try {
- mActionModePopup.dismiss();
- } catch (IllegalArgumentException e) {
- // Pre-v18, there are times when the Window will remove the popup before us.
- // In these cases we need to swallow the resulting exception.
- }
- }
- mActionModePopup = null;
- }
- endOnGoingFadeAnimation();
-
- PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
- if (st != null && st.menu != null) {
- st.menu.close();
- }
- }
-
- /**
- * Clears out internal reference when the action mode is destroyed.
- */
- class ActionModeCallbackWrapperV9 implements ActionMode.Callback {
- private ActionMode.Callback mWrapped;
-
- public ActionModeCallbackWrapperV9(ActionMode.Callback wrapped) {
- mWrapped = wrapped;
- }
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- return mWrapped.onCreateActionMode(mode, menu);
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- return mWrapped.onPrepareActionMode(mode, menu);
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- return mWrapped.onActionItemClicked(mode, item);
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- mWrapped.onDestroyActionMode(mode);
- if (mActionModePopup != null) {
- mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
- }
-
- if (mActionModeView != null) {
- endOnGoingFadeAnimation();
- mFadeAnim = ViewCompat.animate(mActionModeView).alpha(0f);
- mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(View view) {
- mActionModeView.setVisibility(View.GONE);
- if (mActionModePopup != null) {
- mActionModePopup.dismiss();
- } else if (mActionModeView.getParent() instanceof View) {
- ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
- }
- mActionModeView.removeAllViews();
- mFadeAnim.setListener(null);
- mFadeAnim = null;
- }
- });
- }
- if (mAppCompatCallback != null) {
- mAppCompatCallback.onSupportActionModeFinished(mActionMode);
- }
- mActionMode = null;
- }
- }
-
- private final class PanelMenuPresenterCallback implements MenuPresenter.Callback {
- PanelMenuPresenterCallback() {
- }
-
- @Override
- public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
- final Menu parentMenu = menu.getRootMenu();
- final boolean isSubMenu = parentMenu != menu;
- final PanelFeatureState panel = findMenuPanel(isSubMenu ? parentMenu : menu);
- if (panel != null) {
- if (isSubMenu) {
- callOnPanelClosed(panel.featureId, panel, parentMenu);
- closePanel(panel, true);
- } else {
- // Close the panel and only do the callback if the menu is being
- // closed completely, not if opening a sub menu
- closePanel(panel, allMenusAreClosing);
- }
- }
- }
-
- @Override
- public boolean onOpenSubMenu(MenuBuilder subMenu) {
- if (subMenu == null && mHasActionBar) {
- Window.Callback cb = getWindowCallback();
- if (cb != null && !isDestroyed()) {
- cb.onMenuOpened(FEATURE_SUPPORT_ACTION_BAR, subMenu);
- }
- }
- return true;
- }
- }
-
- private final class ActionMenuPresenterCallback implements MenuPresenter.Callback {
- ActionMenuPresenterCallback() {
- }
-
- @Override
- public boolean onOpenSubMenu(MenuBuilder subMenu) {
- Window.Callback cb = getWindowCallback();
- if (cb != null) {
- cb.onMenuOpened(FEATURE_SUPPORT_ACTION_BAR, subMenu);
- }
- return true;
- }
-
- @Override
- public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
- checkCloseActionMenu(menu);
- }
- }
-
- protected static final class PanelFeatureState {
-
- /** Feature ID for this panel. */
- int featureId;
-
- int background;
-
- int gravity;
-
- int x;
-
- int y;
-
- int windowAnimations;
-
- /** Dynamic state of the panel. */
- ViewGroup decorView;
-
- /** The panel that we are actually showing. */
- View shownPanelView;
-
- /** The panel that was returned by onCreatePanelView(). */
- View createdPanelView;
-
- /** Use {@link #setMenu} to set this. */
- MenuBuilder menu;
-
- ListMenuPresenter listMenuPresenter;
-
- Context listPresenterContext;
-
- /**
- * Whether the panel has been prepared (see
- * {@link #preparePanel}).
- */
- boolean isPrepared;
-
- /**
- * Whether an item's action has been performed. This happens in obvious
- * scenarios (user clicks on menu item), but can also happen with
- * chording menu+(shortcut key).
- */
- boolean isHandled;
-
- boolean isOpen;
-
- public boolean qwertyMode;
-
- boolean refreshDecorView;
-
- boolean refreshMenuContent;
-
- boolean wasLastOpen;
-
- /**
- * Contains the state of the menu when told to freeze.
- */
- Bundle frozenMenuState;
-
- /**
- * Contains the state of associated action views when told to freeze.
- * These are saved across invalidations.
- */
- Bundle frozenActionViewState;
-
- PanelFeatureState(int featureId) {
- this.featureId = featureId;
-
- refreshDecorView = false;
- }
-
- public boolean hasPanelItems() {
- if (shownPanelView == null) return false;
- if (createdPanelView != null) return true;
-
- return listMenuPresenter.getAdapter().getCount() > 0;
- }
-
- /**
- * Unregister and free attached MenuPresenters. They will be recreated as needed.
- */
- public void clearMenuPresenters() {
- if (menu != null) {
- menu.removeMenuPresenter(listMenuPresenter);
- }
- listMenuPresenter = null;
- }
-
- void setStyle(Context context) {
- final TypedValue outValue = new TypedValue();
- final Resources.Theme widgetTheme = context.getResources().newTheme();
- widgetTheme.setTo(context.getTheme());
-
- // First apply the actionBarPopupTheme
- widgetTheme.resolveAttribute(R.attr.actionBarPopupTheme, outValue, true);
- if (outValue.resourceId != 0) {
- widgetTheme.applyStyle(outValue.resourceId, true);
- }
-
- // Now apply the panelMenuListTheme
- widgetTheme.resolveAttribute(R.attr.panelMenuListTheme, outValue, true);
- if (outValue.resourceId != 0) {
- widgetTheme.applyStyle(outValue.resourceId, true);
- } else {
- widgetTheme.applyStyle(R.style.Theme_AppCompat_CompactMenu, true);
- }
-
- context = new ContextThemeWrapper(context, 0);
- context.getTheme().setTo(widgetTheme);
-
- listPresenterContext = context;
-
- TypedArray a = context.obtainStyledAttributes(R.styleable.AppCompatTheme);
- background = a.getResourceId(
- R.styleable.AppCompatTheme_panelBackground, 0);
- windowAnimations = a.getResourceId(
- R.styleable.AppCompatTheme_android_windowAnimationStyle, 0);
- a.recycle();
- }
-
- void setMenu(MenuBuilder menu) {
- if (menu == this.menu) return;
-
- if (this.menu != null) {
- this.menu.removeMenuPresenter(listMenuPresenter);
- }
- this.menu = menu;
- if (menu != null) {
- if (listMenuPresenter != null) menu.addMenuPresenter(listMenuPresenter);
- }
- }
-
- MenuView getListMenuView(MenuPresenter.Callback cb) {
- if (menu == null) return null;
-
- if (listMenuPresenter == null) {
- listMenuPresenter = new ListMenuPresenter(listPresenterContext,
- R.layout.abc_list_menu_item_layout);
- listMenuPresenter.setCallback(cb);
- menu.addMenuPresenter(listMenuPresenter);
- }
-
- MenuView result = listMenuPresenter.getMenuView(decorView);
-
- return result;
- }
-
- Parcelable onSaveInstanceState() {
- SavedState savedState = new SavedState();
- savedState.featureId = featureId;
- savedState.isOpen = isOpen;
-
- if (menu != null) {
- savedState.menuState = new Bundle();
- menu.savePresenterStates(savedState.menuState);
- }
-
- return savedState;
- }
-
- void onRestoreInstanceState(Parcelable state) {
- SavedState savedState = (SavedState) state;
- featureId = savedState.featureId;
- wasLastOpen = savedState.isOpen;
- frozenMenuState = savedState.menuState;
-
- shownPanelView = null;
- decorView = null;
- }
-
- void applyFrozenState() {
- if (menu != null && frozenMenuState != null) {
- menu.restorePresenterStates(frozenMenuState);
- frozenMenuState = null;
- }
- }
-
- private static class SavedState implements Parcelable {
- int featureId;
- boolean isOpen;
- Bundle menuState;
-
- SavedState() {
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(featureId);
- dest.writeInt(isOpen ? 1 : 0);
-
- if (isOpen) {
- dest.writeBundle(menuState);
- }
- }
-
- static SavedState readFromParcel(Parcel source, ClassLoader loader) {
- SavedState savedState = new SavedState();
- savedState.featureId = source.readInt();
- savedState.isOpen = source.readInt() == 1;
-
- if (savedState.isOpen) {
- savedState.menuState = source.readBundle(loader);
- }
-
- return savedState;
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return readFromParcel(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return readFromParcel(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
- }
-
- private class ListMenuDecorView extends ContentFrameLayout {
- public ListMenuDecorView(Context context) {
- super(context);
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- return AppCompatDelegateImplV9.this.dispatchKeyEvent(event)
- || super.dispatchKeyEvent(event);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- int action = event.getAction();
- if (action == MotionEvent.ACTION_DOWN) {
- int x = (int) event.getX();
- int y = (int) event.getY();
- if (isOutOfBounds(x, y)) {
- closePanel(Window.FEATURE_OPTIONS_PANEL);
- return true;
- }
- }
- return super.onInterceptTouchEvent(event);
- }
-
- @Override
- public void setBackgroundResource(int resid) {
- setBackgroundDrawable(AppCompatResources.getDrawable(getContext(), resid));
- }
-
- private boolean isOutOfBounds(int x, int y) {
- return x < -5 || y < -5 || x > (getWidth() + 5) || y > (getHeight() + 5);
- }
- }
-}
diff --git a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java
index 21ba27e..8aa54d1 100644
--- a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java
+++ b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java
@@ -1375,8 +1375,6 @@
| ((START | END) << DIRECTION_FLAG_COUNT)
| ((START | END) << (2 * DIRECTION_FLAG_COUNT));
- private static final ItemTouchUIUtil sUICallback;
-
private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT
| ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT)
| ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT));
@@ -1403,14 +1401,6 @@
private int mCachedMaxScrollSpeed = -1;
- static {
- if (Build.VERSION.SDK_INT >= 21) {
- sUICallback = new ItemTouchUIUtilImpl.Api21Impl();
- } else {
- sUICallback = new ItemTouchUIUtilImpl.BaseImpl();
- }
- }
-
/**
* Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for
* visual
@@ -1454,7 +1444,7 @@
@SuppressWarnings("WeakerAccess")
@NonNull
public static ItemTouchUIUtil getDefaultUIUtil() {
- return sUICallback;
+ return ItemTouchUIUtilImpl.INSTANCE;
}
/**
@@ -1884,7 +1874,7 @@
*/
public void onSelectedChanged(@Nullable ViewHolder viewHolder, int actionState) {
if (viewHolder != null) {
- sUICallback.onSelected(viewHolder.itemView);
+ ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView);
}
}
@@ -2026,7 +2016,7 @@
* @param viewHolder The View that was interacted by the user.
*/
public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
- sUICallback.clearView(viewHolder.itemView);
+ ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView);
}
/**
@@ -2059,8 +2049,8 @@
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
@NonNull ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
- sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
- isCurrentlyActive);
+ ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY,
+ actionState, isCurrentlyActive);
}
/**
@@ -2093,8 +2083,8 @@
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
- sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
- isCurrentlyActive);
+ ItemTouchUIUtilImpl.INSTANCE.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY,
+ actionState, isCurrentlyActive);
}
/**
diff --git a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java
index c41f9cb..f41df9b 100644
--- a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java
+++ b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java
@@ -17,6 +17,7 @@
package androidx.recyclerview.widget;
import android.graphics.Canvas;
+import android.os.Build;
import android.view.View;
import androidx.core.view.ViewCompat;
@@ -26,11 +27,13 @@
* Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
* public API, which is not desired in this case.
*/
-class ItemTouchUIUtilImpl {
- static class Api21Impl extends BaseImpl {
- @Override
- public void onDraw(Canvas c, RecyclerView recyclerView, View view,
- float dX, float dY, int actionState, boolean isCurrentlyActive) {
+class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
+ static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl();
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
+ int actionState, boolean isCurrentlyActive) {
+ if (Build.VERSION.SDK_INT >= 21) {
if (isCurrentlyActive) {
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
if (originalElevation == null) {
@@ -40,60 +43,48 @@
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
- super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
}
- private float findMaxElevation(RecyclerView recyclerView, View itemView) {
- final int childCount = recyclerView.getChildCount();
- float max = 0;
- for (int i = 0; i < childCount; i++) {
- final View child = recyclerView.getChildAt(i);
- if (child == itemView) {
- continue;
- }
- final float elevation = ViewCompat.getElevation(child);
- if (elevation > max) {
- max = elevation;
- }
+ view.setTranslationX(dX);
+ view.setTranslationY(dY);
+ }
+
+ private static float findMaxElevation(RecyclerView recyclerView, View itemView) {
+ final int childCount = recyclerView.getChildCount();
+ float max = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View child = recyclerView.getChildAt(i);
+ if (child == itemView) {
+ continue;
}
- return max;
+ final float elevation = ViewCompat.getElevation(child);
+ if (elevation > max) {
+ max = elevation;
+ }
}
+ return max;
+ }
- @Override
- public void clearView(View view) {
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
+ int actionState, boolean isCurrentlyActive) {
+ }
+
+ @Override
+ public void clearView(View view) {
+ if (Build.VERSION.SDK_INT >= 21) {
final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
if (tag != null && tag instanceof Float) {
ViewCompat.setElevation(view, (Float) tag);
}
view.setTag(R.id.item_touch_helper_previous_elevation, null);
- super.clearView(view);
}
+
+ view.setTranslationX(0f);
+ view.setTranslationY(0f);
}
- static class BaseImpl implements ItemTouchUIUtil {
-
- @Override
- public void clearView(View view) {
- view.setTranslationX(0f);
- view.setTranslationY(0f);
- }
-
- @Override
- public void onSelected(View view) {
-
- }
-
- @Override
- public void onDraw(Canvas c, RecyclerView recyclerView, View view,
- float dX, float dY, int actionState, boolean isCurrentlyActive) {
- view.setTranslationX(dX);
- view.setTranslationY(dY);
- }
-
- @Override
- public void onDrawOver(Canvas c, RecyclerView recyclerView,
- View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {
-
- }
+ @Override
+ public void onSelected(View view) {
}
}
diff --git a/viewpager2/src/androidTest/AndroidManifest.xml b/viewpager2/src/androidTest/AndroidManifest.xml
index d1332fa..8e7596b 100755
--- a/viewpager2/src/androidTest/AndroidManifest.xml
+++ b/viewpager2/src/androidTest/AndroidManifest.xml
@@ -19,6 +19,7 @@
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<application android:supportsRtl="true">
- <activity android:name="androidx.viewpager2.widget.TestActivity"/>
+ <activity android:name="androidx.viewpager2.widget.swipe.ViewAdapterActivity"/>
+ <activity android:name="androidx.viewpager2.widget.swipe.FragmentAdapterActivity"/>
</application>
</manifest>
\ No newline at end of file
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BasicTest.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BasicTest.java
new file mode 100644
index 0000000..483317b
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BasicTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayWithSize;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.UUID;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BasicTest {
+ @Rule
+ public ExpectedException mExpectedException = ExpectedException.none();
+
+ @Test
+ public void test_recyclerViewAdapter_pageFillEnforced() {
+ mExpectedException.expect(IllegalStateException.class);
+ mExpectedException.expectMessage(
+ "Item's root view must fill the whole ViewPager2 (use match_parent)");
+
+ ViewPager2 viewPager = new ViewPager2(InstrumentationRegistry.getContext());
+ viewPager.setAdapter(new RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
+ View view = new View(parent.getContext());
+ view.setLayoutParams(new ViewGroup.LayoutParams(50, 50)); // arbitrary fixed size
+ return new RecyclerView.ViewHolder(view) {
+ };
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ // do nothing
+ }
+
+ @Override
+ public int getItemCount() {
+ return 1;
+ }
+ });
+
+ viewPager.measure(0, 0); // equivalent of unspecified
+ }
+
+ @Test
+ public void test_childrenNotAllowed() throws Exception {
+ mExpectedException.expect(IllegalStateException.class);
+ mExpectedException.expectMessage("ViewPager2 does not support direct child views");
+
+ Context context = InstrumentationRegistry.getContext();
+ ViewPager2 viewPager = new ViewPager2(context);
+ viewPager.addView(new View(context));
+ }
+
+ @Test
+ public void test_saveStateParcel_createRestore() throws Throwable {
+ // given
+ Bundle superState = createIntBundle(42);
+ ViewPager2.SavedState state = new ViewPager2.SavedState(superState);
+ state.mRecyclerViewId = 700;
+ state.mAdapterState = new Parcelable[]{createIntBundle(1), createIntBundle(2),
+ createIntBundle(3)};
+
+ // when
+ Parcel parcel = Parcel.obtain();
+ state.writeToParcel(parcel, 0);
+ final String parcelSuffix = UUID.randomUUID().toString();
+ parcel.writeString(parcelSuffix); // to verify parcel boundaries
+ parcel.setDataPosition(0);
+ ViewPager2.SavedState recreatedState = ViewPager2.SavedState.CREATOR.createFromParcel(
+ parcel);
+
+ // then
+ assertThat("Parcel reading should not go out of bounds", parcel.readString(),
+ equalTo(parcelSuffix));
+ assertThat("All of the parcel should be read", parcel.dataAvail(), equalTo(0));
+ assertThat(recreatedState.mRecyclerViewId, equalTo(700));
+ assertThat(recreatedState.mAdapterState, arrayWithSize(3));
+ assertThat((int) ((Bundle) recreatedState.getSuperState()).get("key"), equalTo(42));
+ assertThat((int) ((Bundle) recreatedState.mAdapterState[0]).get("key"), equalTo(1));
+ assertThat((int) ((Bundle) recreatedState.mAdapterState[1]).get("key"), equalTo(2));
+ assertThat((int) ((Bundle) recreatedState.mAdapterState[2]).get("key"), equalTo(3));
+ }
+
+ private Bundle createIntBundle(int value) {
+ Bundle bundle = new Bundle(1);
+ bundle.putInt("key", value);
+ return bundle;
+ }
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SwipeTest.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SwipeTest.java
new file mode 100644
index 0000000..95bf351
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/SwipeTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static android.view.View.OVER_SCROLL_NEVER;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import android.os.Build;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.testutils.FragmentActivityUtils;
+import androidx.viewpager2.test.R;
+import androidx.viewpager2.widget.swipe.BaseActivity;
+import androidx.viewpager2.widget.swipe.FragmentAdapterActivity;
+import androidx.viewpager2.widget.swipe.PageSwiper;
+import androidx.viewpager2.widget.swipe.ViewAdapterActivity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class SwipeTest {
+ private static final List<Class<? extends BaseActivity>> TEST_ACTIVITIES_ALL = asList(
+ ViewAdapterActivity.class, FragmentAdapterActivity.class);
+ private static final Set<Integer> NO_CONFIG_CHANGES = Collections.emptySet();
+ private static final List<Pair<Integer, Integer>> NO_MUTATIONS = Collections.emptyList();
+ private static final boolean RANDOM_PASS_ENABLED = false;
+
+ private final TestConfig mTestConfig;
+ private ActivityTestRule<? extends BaseActivity> mActivityTestRule;
+ private PageSwiper mSwiper;
+
+ public SwipeTest(TestConfig testConfig) {
+ mTestConfig = testConfig;
+ }
+
+ @Test
+ public void test() throws Throwable {
+ BaseActivity activity = mActivityTestRule.getActivity();
+
+ final int[] expectedValues = new int[mTestConfig.mTotalPages];
+ for (int i = 0; i < mTestConfig.mTotalPages; i++) {
+ expectedValues[i] = i;
+ }
+
+ int currentPage = 0, currentStep = 0;
+ assertStateCorrect(expectedValues[currentPage], activity);
+ for (int nextPage : mTestConfig.mPageSequence) {
+ // value change
+ if (mTestConfig.mStepToNewValue.containsKey(currentStep)) {
+ expectedValues[currentPage] = mTestConfig.mStepToNewValue.get(currentStep);
+ activity.updatePage(currentPage, expectedValues[currentPage]);
+ assertStateCorrect(expectedValues[currentPage], activity);
+ }
+
+ // config change
+ if (mTestConfig.mConfigChangeSteps.contains(currentStep++)) {
+ activity = FragmentActivityUtils.recreateActivity(mActivityTestRule, activity);
+ assertStateCorrect(expectedValues[currentPage], activity);
+ }
+
+ // page swipe
+ mSwiper.swipe(currentPage, nextPage);
+ currentPage = nextPage;
+ assertStateCorrect(expectedValues[currentPage], activity);
+ }
+ }
+
+ private void assertStateCorrect(int expectedValue, BaseActivity activity) {
+ onView(allOf(withId(R.id.text_view), isDisplayed())).check(
+ matches(withText(String.valueOf(expectedValue))));
+ activity.validateState();
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static List<TestConfig> getParams() {
+ List<TestConfig> tests = new ArrayList<>();
+
+ if (RANDOM_PASS_ENABLED) { // run locally after making larger changes
+ tests.addAll(generateRandomTests());
+ }
+
+ for (Class<? extends BaseActivity> activityClass : TEST_ACTIVITIES_ALL) {
+ tests.add(new TestConfig("full pass", asList(1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0),
+ NO_CONFIG_CHANGES, NO_MUTATIONS, 8, activityClass));
+
+ tests.add(new TestConfig("swipe beyond edge pages",
+ asList(0, 0, 1, 2, 3, 3, 3, 2, 1, 0, 0, 0), NO_CONFIG_CHANGES, NO_MUTATIONS, 4,
+ activityClass));
+
+ tests.add(new TestConfig("config change", asList(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0),
+ asList(3, 5, 7), NO_MUTATIONS, 7, activityClass));
+
+ tests.add(
+ new TestConfig("regression1", asList(1, 2, 3, 2, 1, 2, 3, 4), NO_CONFIG_CHANGES,
+ NO_MUTATIONS, 10, activityClass));
+
+ tests.add(new TestConfig("regression2", asList(1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 5),
+ NO_CONFIG_CHANGES, NO_MUTATIONS, 10, activityClass));
+
+ tests.add(new TestConfig("regression3", asList(1, 2, 3, 2, 1, 2, 3, 2, 1, 0),
+ NO_CONFIG_CHANGES, NO_MUTATIONS, 10, activityClass));
+ }
+
+ // mutations only apply to Fragment state persistence
+ tests.add(new TestConfig("mutations", asList(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0),
+ singletonList(8),
+ asList(Pair.create(0, 999), Pair.create(1, 100), Pair.create(3, 300),
+ Pair.create(5, 500)), 7, FragmentAdapterActivity.class));
+
+ return checkTestNamesUnique(tests);
+ }
+
+ private static Collection<TestConfig> generateRandomTests() {
+ List<TestConfig> result = new ArrayList<>();
+
+ int id = 0;
+ for (int i = 0; i < 10; i++) {
+ // both adapters
+ for (Class<? extends BaseActivity> adapterClass : TEST_ACTIVITIES_ALL) {
+ result.add(createRandomTest(id++, 8, 50, 0, 0, 0.875, adapterClass));
+ result.add(createRandomTest(id++, 8, 10, 0.5, 0, 0.875, adapterClass));
+ }
+
+ // fragment adapter specific
+ result.add(
+ createRandomTest(id++, 8, 50, 0, 0.125, 0.875, FragmentAdapterActivity.class));
+ result.add(createRandomTest(id++, 8, 10, 0.5, 0.125, 0.875,
+ FragmentAdapterActivity.class));
+ }
+
+ return result;
+ }
+
+ /**
+ * @param advanceProbability determines the probability of a swipe direction being towards
+ * the next edge - e.g. if we start from the left, it's the
+ * probability that the next swipe will go right. <p> Setting it to
+ * values closer to 0.5 results in a lot of back and forth, while
+ * setting it closer to 1.0 results in going edge to edge with few
+ * back-swipes.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private static TestConfig createRandomTest(int id, int totalPages, int sequenceLength,
+ double configChangeProbability, double mutationProbability, double advanceProbability,
+ Class<? extends BaseActivity> activityClass) {
+ Random random = new Random();
+
+ List<Integer> pageSequence = new ArrayList<>();
+ List<Integer> configChanges = new ArrayList<>();
+ List<Pair<Integer, Integer>> stepToNewValue = new ArrayList<>();
+
+ int pageIx = 0;
+ Double goRightProbability = null;
+ for (int currentStep = 0; currentStep < sequenceLength; currentStep++) {
+ if (random.nextDouble() < configChangeProbability) {
+ configChanges.add(currentStep);
+ }
+
+ if (random.nextDouble() < mutationProbability) {
+ stepToNewValue.add(Pair.create(currentStep, random.nextInt(10_000)));
+ }
+
+ boolean goRight;
+ if (pageIx == 0) {
+ goRight = true;
+ goRightProbability = advanceProbability;
+ } else if (pageIx == totalPages - 1) { // last page
+ goRight = false;
+ goRightProbability = 1 - advanceProbability;
+ } else {
+ goRight = random.nextDouble() < goRightProbability;
+ }
+ pageSequence.add(goRight ? ++pageIx : --pageIx);
+ }
+
+ return new TestConfig("random_" + id, pageSequence, configChanges, stepToNewValue,
+ totalPages, activityClass);
+ }
+
+ private static List<TestConfig> checkTestNamesUnique(List<TestConfig> configs) {
+ Set<String> names = new HashSet<>();
+ for (TestConfig config : configs) {
+ names.add(config.toString());
+ }
+ assertThat(names.size(), is(configs.size()));
+ return configs;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ Log.i(getClass().getSimpleName(), mTestConfig.toFullSpecString());
+
+ mActivityTestRule = new ActivityTestRule<>(mTestConfig.mActivityClass, true, false);
+ mActivityTestRule.launchActivity(BaseActivity.createIntent(mTestConfig.mTotalPages));
+
+ ViewPager2 viewPager = mActivityTestRule.getActivity().findViewById(R.id.view_pager);
+ RecyclerView recyclerView = (RecyclerView) viewPager.getChildAt(0); // HACK
+ mSwiper = new PageSwiper(mTestConfig.mTotalPages, recyclerView);
+
+ if (Build.VERSION.SDK_INT < 16) { // TODO(b/71500143): remove temporary workaround
+ RecyclerView mRecyclerView = (RecyclerView) viewPager.getChildAt(0);
+ mRecyclerView.setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
+ onView(withId(R.id.view_pager)).check(matches(isDisplayed()));
+ }
+
+ private static class TestConfig {
+ final String mMessage;
+ final List<Integer> mPageSequence;
+ final Set<Integer> mConfigChangeSteps;
+ /** {@link Map.Entry#getKey()} = step, {@link Map.Entry#getValue()} = new value */
+ final Map<Integer, Integer> mStepToNewValue;
+ final int mTotalPages;
+ final Class<? extends BaseActivity> mActivityClass;
+
+ /**
+ * @param stepToNewValue {@link Pair#first} = step, {@link Pair#second} = new value
+ */
+ TestConfig(String message, List<Integer> pageSequence,
+ Collection<Integer> configChangeSteps,
+ List<Pair<Integer, Integer>> stepToNewValue,
+ int totalPages,
+ Class<? extends BaseActivity> activityClass) {
+ mMessage = message;
+ mPageSequence = pageSequence;
+ mConfigChangeSteps = new HashSet<>(configChangeSteps);
+ mStepToNewValue = mapFromPairList(stepToNewValue);
+ mTotalPages = totalPages;
+ mActivityClass = activityClass;
+ }
+
+ private static Map<Integer, Integer> mapFromPairList(List<Pair<Integer, Integer>> list) {
+ Map<Integer, Integer> result = new HashMap<>();
+ for (Pair<Integer, Integer> pair : list) {
+ Integer prevValueAtKey = result.put(pair.first, pair.second);
+ assertThat("there should be only one value defined for a key", prevValueAtKey,
+ equalTo(null));
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return mActivityClass.getSimpleName() + ": " + mMessage;
+ }
+
+ String toFullSpecString() {
+ return String.format(
+ "Test: %s\nPage sequence: %s\nTotal pages: %s\nMutations {step1:newValue1, "
+ + "step2:newValue2, ...}: %s",
+ toString(),
+ mPageSequence,
+ mTotalPages,
+ mStepToNewValue.toString().replace('=', ':'));
+ }
+ }
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TestActivity.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TestActivity.java
deleted file mode 100644
index 74e2e4a..0000000
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/TestActivity.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 androidx.viewpager2.widget;
-
-import android.os.Bundle;
-
-import androidx.testutils.RecreatedActivity;
-import androidx.viewpager2.test.R;
-
-public class TestActivity extends RecreatedActivity {
- @Override
- public void onCreate(Bundle bundle) {
- super.onCreate(bundle);
- setContentView(R.layout.activity_test_layout);
-
- ViewPager2Tests.sAdapterStrategy.setAdapter((ViewPager2) findViewById(R.id.view_pager));
- }
-}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/ViewPager2Tests.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/ViewPager2Tests.java
deleted file mode 100644
index 48d55f7..0000000
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/ViewPager2Tests.java
+++ /dev/null
@@ -1,669 +0,0 @@
-/*
- * 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 androidx.viewpager2.widget;
-
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-import static android.view.View.OVER_SCROLL_NEVER;
-
-import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
-
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.arrayWithSize;
-import static org.hamcrest.Matchers.greaterThanOrEqualTo;
-import static org.hamcrest.Matchers.lessThanOrEqualTo;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.espresso.ViewAction;
-import android.support.test.espresso.action.ViewActions;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.util.Preconditions;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import androidx.testutils.FragmentActivityUtils;
-import androidx.viewpager2.test.R;
-
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Random;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class ViewPager2Tests {
- private static final Random RANDOM = new Random();
- private static final int[] sColors = {
- Color.parseColor("#BBA9FF00"),
- Color.parseColor("#BB00E87E"),
- Color.parseColor("#BB00C7FF"),
- Color.parseColor("#BBB30CE8"),
- Color.parseColor("#BBFF00D0")};
-
- /** mean of injecting different adapters into {@link TestActivity#onCreate(Bundle)} */
- static AdapterStrategy sAdapterStrategy;
-
- interface AdapterStrategy {
- void setAdapter(ViewPager2 viewPager);
- }
-
- @Rule
- public final ActivityTestRule<TestActivity> mActivityTestRule;
- @Rule
- public ExpectedException mExpectedException = ExpectedException.none();
-
- // allows to wait until swipe operation is finished (Smooth Scroller done)
- private CountDownLatch mStableAfterSwipe;
-
- public ViewPager2Tests() {
- mActivityTestRule = new ActivityTestRule<>(TestActivity.class, true, false);
- }
-
- private void setUpActivity(AdapterStrategy adapterStrategy) {
- sAdapterStrategy = Preconditions.checkNotNull(adapterStrategy);
- mActivityTestRule.launchActivity(null);
-
- ViewPager2 viewPager = mActivityTestRule.getActivity().findViewById(R.id.view_pager);
- viewPager.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- // coming to idle from another state (dragging or setting) means we're stable now
- if (newState == SCROLL_STATE_IDLE) {
- mStableAfterSwipe.countDown();
- }
- }
- });
-
- if (Build.VERSION.SDK_INT < 16) { // TODO(b/71500143): remove temporary workaround
- RecyclerView mRecyclerView = (RecyclerView) viewPager.getChildAt(0);
- mRecyclerView.setOverScrollMode(OVER_SCROLL_NEVER);
- }
-
- onView(withId(viewPager.getId())).check(matches(isDisplayed()));
- }
-
- @Before
- public void setUp() {
- sAdapterStrategy = null;
-
- final long seed = RANDOM.nextLong();
- RANDOM.setSeed(seed);
- Log.i(getClass().getName(), "Random seed: " + seed);
- }
-
- public static class PageFragment extends Fragment {
- private static final String KEY_VALUE = "value";
-
- public interface EventListener {
- void onEvent(PageFragment fragment);
-
- EventListener NO_OP = new EventListener() {
- @Override
- public void onEvent(PageFragment fragment) {
- // do nothing
- }
- };
- }
-
- private EventListener mOnAttachListener = EventListener.NO_OP;
- private EventListener mOnDestroyListener = EventListener.NO_OP;
-
- private int mPosition;
- private int mValue;
-
- public static PageFragment create(int position, int value) {
- PageFragment result = new PageFragment();
- Bundle args = new Bundle(1);
- args.putInt(KEY_VALUE, value);
- result.setArguments(args);
- result.mPosition = position;
- return result;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mOnAttachListener.onEvent(this);
- }
-
- @NonNull
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.item_test_layout, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- Bundle data = savedInstanceState != null ? savedInstanceState : getArguments();
- setValue(data.getInt(KEY_VALUE));
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mOnDestroyListener.onEvent(this);
- }
-
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- outState.putInt(KEY_VALUE, mValue);
- }
-
- public void setValue(int value) {
- mValue = value;
- TextView textView = getView().findViewById(R.id.text_view);
- applyViewValue(textView, mValue);
- }
- }
-
- private static void applyViewValue(TextView textView, int value) {
- textView.setText(String.valueOf(value));
- textView.setBackgroundColor(getColor(value));
- }
-
- private static int getColor(int value) {
- return sColors[value % sColors.length];
- }
-
- @Test
- public void fragmentAdapter_fullPass() throws Throwable {
- testFragmentLifecycle(8, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0));
- }
-
- @Test
- public void fragmentAdapter_activityRecreation() throws Throwable {
- testFragmentLifecycle(7, Arrays.asList(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0),
- PageMutator.NO_OP, new HashSet<>(Arrays.asList(3, 5, 7)));
- }
-
- @Test
- public void fragmentAdapter_random() throws Throwable {
- final int totalPages = 8; // increase when stress testing locally
- final int sequenceLength = 20; // increase when stress testing locally
- testFragmentLifecycle_random(totalPages, sequenceLength, PageMutator.NO_OP);
- }
-
- @Test
- public void fragmentAdapter_random_withMutations() throws Throwable {
- final int totalPages = 8; // increase when stress testing locally
- final int sequenceLength = 20; // increase when stress testing locally
- testFragmentLifecycle_random(totalPages, sequenceLength, PageMutator.RANDOM);
- }
-
- private void testFragmentLifecycle_random(int totalPages, int sequenceLength,
- PageMutator pageMutator) throws Throwable {
- List<Integer> pageSequence = generateRandomPageSequence(totalPages, sequenceLength);
-
- Log.i(getClass().getName(),
- String.format("Testing with a sequence [%s]", TextUtils.join(", ", pageSequence)));
-
- testFragmentLifecycle(totalPages, pageSequence, pageMutator,
- Collections.<Integer>emptySet());
- }
-
- @NonNull
- private List<Integer> generateRandomPageSequence(int totalPages, int sequenceLength) {
- List<Integer> pageSequence = new ArrayList<>(sequenceLength);
-
- int pageIx = 0;
- Double goRightProbability = null;
- while (pageSequence.size() != sequenceLength) {
- boolean goRight;
- if (pageIx == 0) {
- goRight = true;
- goRightProbability = 0.7;
- } else if (pageIx == totalPages - 1) { // last page
- goRight = false;
- goRightProbability = 0.3;
- } else {
- goRight = RANDOM.nextDouble() < goRightProbability;
- }
-
- pageSequence.add(goRight ? ++pageIx : --pageIx);
- }
-
- return pageSequence;
- }
-
- /**
- * Test added when caught a bug: after the last swipe: actual=6, expected=4
- * <p>
- * Bug was caused by an invalid test assumption (new Fragment value can be inferred from number
- * of instances created) - invalid in a case when we sometimes create Fragments off-screen and
- * end up scrapping them.
- **/
- @Test
- public void fragmentAdapter_regression1() throws Throwable {
- testFragmentLifecycle(10, Arrays.asList(1, 2, 3, 2, 1, 2, 3, 4));
- }
-
- /**
- * Test added when caught a bug: after the last swipe: actual=4, expected=5
- * <p>
- * Bug was caused by mSavedStates.add(position, ...) instead of mSavedStates.set(position, ...)
- **/
- @Test
- public void fragmentAdapter_regression2() throws Throwable {
- testFragmentLifecycle(10, Arrays.asList(1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 5));
- }
-
- /**
- * Test added when caught a bug: after the last swipe: ArrayIndexOutOfBoundsException: length=5;
- * index=-1 at androidx.viewpager2.widget.tests.ViewPager2Tests$PageFragment.onCreateView
- * <p>
- * Bug was caused by always saving states of unattached fragments as null (even if there was a
- * valid previously saved state)
- */
- @Test
- public void fragmentAdapter_regression3() throws Throwable {
- testFragmentLifecycle(10, Arrays.asList(1, 2, 3, 2, 1, 2, 3, 2, 1, 0));
- }
-
- /** Goes left on left edge / right on right edge */
- @Test
- public void fragmentAdapter_edges() throws Throwable {
- testFragmentLifecycle(4, Arrays.asList(0, 0, 1, 2, 3, 3, 3, 2, 1, 0, 0, 0));
- }
-
- private interface PageMutator {
- void mutate(PageFragment fragment);
-
- PageMutator NO_OP = new PageMutator() {
- @Override
- public void mutate(PageFragment fragment) {
- // do nothing
- }
- };
-
- /** At random modifies the page under Fragment */
- PageMutator RANDOM = new PageMutator() {
- @Override
- public void mutate(PageFragment fragment) {
- Random random = ViewPager2Tests.RANDOM;
- if (random.nextDouble() < 0.125) {
- int delta = (1 + random.nextInt(5)) * sColors.length;
- fragment.setValue(fragment.mValue + delta);
- }
- }
- };
- }
-
- /** @see this#testFragmentLifecycle(int, List, PageMutator, Set) */
- private void testFragmentLifecycle(final int totalPages, List<Integer> pageSequence)
- throws Throwable {
- testFragmentLifecycle(totalPages, pageSequence, PageMutator.NO_OP,
- Collections.<Integer>emptySet());
- }
-
- /**
- * Verifies:
- * <ul>
- * <li>page content / background
- * <li>maximum number of Fragments held in memory
- * <li>Fragment state saving / restoring
- * </ul>
- */
- private void testFragmentLifecycle(final int totalPages, List<Integer> pageSequence,
- final PageMutator pageMutator, Set<Integer> activityRecreateSteps) throws Throwable {
- final AtomicInteger attachCount = new AtomicInteger(0);
- final AtomicInteger destroyCount = new AtomicInteger(0);
- final boolean[] wasEverAttached = new boolean[totalPages];
- final PageFragment[] fragments = new PageFragment[totalPages];
-
- final int[] expectedValues = new int[totalPages];
- for (int i = 0; i < totalPages; i++) {
- expectedValues[i] = i;
- }
-
- setUpActivity(new AdapterStrategy() {
- @Override
- public void setAdapter(ViewPager2 viewPager) {
- viewPager.setAdapter(
- ((FragmentActivity) viewPager.getContext()).getSupportFragmentManager(),
- new ViewPager2.FragmentProvider() {
- @Override
- public Fragment getItem(final int position) {
- // if the fragment was attached in the past, it means we have
- // provided it with the correct value already; give a dummy one
- // to prove state save / restore functionality works
- int value = wasEverAttached[position] ? -1 : position;
- PageFragment fragment = PageFragment.create(position, value);
-
- fragment.mOnAttachListener = new PageFragment.EventListener() {
- @Override
- public void onEvent(PageFragment fragment) {
- attachCount.incrementAndGet();
- wasEverAttached[fragment.mPosition] = true;
- }
- };
-
- fragment.mOnDestroyListener = new PageFragment.EventListener() {
- @Override
- public void onEvent(PageFragment fragment) {
- destroyCount.incrementAndGet();
- }
- };
-
- fragments[position] = fragment;
- return fragment;
- }
-
- @Override
- public int getCount() {
- return totalPages;
- }
- }, ViewPager2.FragmentRetentionPolicy.SAVE_STATE);
- }
- });
-
- final AtomicInteger currentPage = new AtomicInteger(0);
- verifyCurrentPage(expectedValues[currentPage.get()]);
- int stepCounter = 0;
- for (int nextPage : pageSequence) {
- if (activityRecreateSteps.contains(stepCounter++)) {
- FragmentActivityUtils.recreateActivity(mActivityTestRule,
- mActivityTestRule.getActivity());
- }
- swipe(currentPage.get(), nextPage, totalPages);
- currentPage.set(nextPage);
- verifyCurrentPage(expectedValues[currentPage.get()]);
-
- // TODO: validate Fragments that are instantiated, but not attached. No destruction
- // steps are done to them - they're just left to the Garbage Collector. Maybe
- // WeakReferences could help, but the GC behaviour is not predictable. Alternatively,
- // we could only create Fragments onAttach, but there is a potential performance
- // trade-off.
- assertThat(attachCount.get() - destroyCount.get(), isBetween(1, 4));
-
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- final int page = currentPage.get();
- PageFragment fragment = fragments[page];
- pageMutator.mutate(fragment);
- expectedValues[page] = fragment.mValue;
- }
- });
- }
- }
-
- private void swipe(int currentPageIx, int nextPageIx, int totalPages)
- throws InterruptedException {
- if (nextPageIx >= totalPages) {
- throw new IllegalArgumentException("Invalid nextPageIx: >= totalPages.");
- }
-
- if (currentPageIx == nextPageIx) { // dedicated for testing edge behaviour
- if (nextPageIx == 0) {
- swipeRight(); // bounce off the left edge
- return;
- }
- if (nextPageIx == totalPages - 1) { // bounce off the right edge
- swipeLeft();
- return;
- }
- throw new IllegalArgumentException(
- "Invalid sequence. Not on an edge, and currentPageIx/nextPageIx pages same.");
- }
-
- if (Math.abs(nextPageIx - currentPageIx) > 1) {
- throw new IllegalArgumentException(
- "Specified nextPageIx not adjacent to the current page.");
- }
-
- if (nextPageIx > currentPageIx) {
- swipeLeft();
- } else {
- swipeRight();
- }
- }
-
- private Matcher<Integer> isBetween(int min, int max) {
- return allOf(greaterThanOrEqualTo(min), lessThanOrEqualTo(max));
- }
-
- @Test
- public void viewAdapter_edges() throws Throwable {
- testViewAdapter(4, Arrays.asList(0, 0, 1, 2, 3, 3, 3, 2, 1, 0, 0, 0),
- Collections.<Integer>emptySet());
- }
-
- @Test
- public void viewAdapter_fullPass() throws Throwable {
- testViewAdapter(8, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1, 0),
- Collections.<Integer>emptySet());
- }
-
- @Test
- public void viewAdapter_activityRecreation() throws Throwable {
- testViewAdapter(7,
- Arrays.asList(1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0),
- new HashSet<>(Arrays.asList(3, 5, 7)));
- }
-
- @Test
- public void saveState_parcelWriteRestore() throws Throwable {
- // given
- Bundle superState = createIntBundle(42);
- ViewPager2.SavedState state = new ViewPager2.SavedState(superState);
- state.mRecyclerViewId = 700;
- state.mAdapterState = new Parcelable[]{createIntBundle(1), createIntBundle(2),
- createIntBundle(3)};
-
- // when
- Parcel parcel = Parcel.obtain();
- state.writeToParcel(parcel, 0);
- final String parcelSuffix = UUID.randomUUID().toString();
- parcel.writeString(parcelSuffix); // to verify parcel boundaries
- parcel.setDataPosition(0);
- ViewPager2.SavedState recreatedState = ViewPager2.SavedState.CREATOR.createFromParcel(
- parcel);
-
- // then
- assertThat("Parcel reading should not go out of bounds", parcel.readString(),
- equalTo(parcelSuffix));
- assertThat("All of the parcel should be read", parcel.dataAvail(), equalTo(0));
- assertThat(recreatedState.mRecyclerViewId, equalTo(700));
- assertThat(recreatedState.mAdapterState, arrayWithSize(3));
- assertThat((int) ((Bundle) recreatedState.getSuperState()).get("key"), equalTo(42));
- assertThat((int) ((Bundle) recreatedState.mAdapterState[0]).get("key"), equalTo(1));
- assertThat((int) ((Bundle) recreatedState.mAdapterState[1]).get("key"), equalTo(2));
- assertThat((int) ((Bundle) recreatedState.mAdapterState[2]).get("key"), equalTo(3));
- }
-
- private Bundle createIntBundle(int value) {
- Bundle bundle = new Bundle(1);
- bundle.putInt("key", value);
- return bundle;
- }
-
- private void testViewAdapter(final int totalPages, List<Integer> pageSequence,
- Set<Integer> activityRecreateSteps) throws InterruptedException {
- setUpActivity(new AdapterStrategy() {
- @Override
- public void setAdapter(final ViewPager2 viewPager) {
- viewPager.setAdapter(
- new Adapter<ViewHolder>() {
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
- int viewType) {
- return new ViewHolder(
- LayoutInflater.from(viewPager.getContext()).inflate(
- R.layout.item_test_layout, parent, false)) {
- };
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- TextView view = (TextView) holder.itemView;
- applyViewValue(view, position);
- }
-
- @Override
- public int getItemCount() {
- return totalPages;
- }
- });
- }
- });
-
- verifyCurrentPage(0);
- int currentPage = 0;
- int stepCounter = 0;
- for (int nextPage : pageSequence) {
- if (activityRecreateSteps.contains(stepCounter++)) {
- FragmentActivityUtils.recreateActivity(mActivityTestRule,
- mActivityTestRule.getActivity());
- }
- swipe(currentPage, nextPage, totalPages);
- currentPage = nextPage;
- verifyCurrentPage(currentPage);
- }
- }
-
- /**
- * Verifies that the current page displays the correct value and has the correct color.
- *
- * @param expectedPageValue value expected to be displayed on the page
- */
- private void verifyCurrentPage(int expectedPageValue) {
- onView(allOf(withId(R.id.text_view), isDisplayed())).check(
- matches(allOf(withText(String.valueOf(expectedPageValue)),
- new BackgroundColorMatcher(getColor(expectedPageValue)))));
- }
-
- private static class BackgroundColorMatcher extends BaseMatcher<View> {
- private final int mColor;
-
- BackgroundColorMatcher(int color) {
- mColor = color;
- }
-
- @Override
- public void describeTo(Description description) {
- description.appendText("should have background color: ").appendValue(mColor);
- }
-
- @Override
- public boolean matches(Object item) {
- ColorDrawable background = (ColorDrawable) ((View) item).getBackground();
- return background.getColor() == mColor;
- }
- }
-
- private void swipeLeft() throws InterruptedException {
- performSwipe(ViewActions.swipeLeft());
- }
-
- private void swipeRight() throws InterruptedException {
- performSwipe(ViewActions.swipeRight());
- }
-
- private void performSwipe(ViewAction swipeAction) throws InterruptedException {
- mStableAfterSwipe = new CountDownLatch(1);
- onView(allOf(isDisplayed(), withId(R.id.text_view))).perform(swipeAction);
- mStableAfterSwipe.await(1, TimeUnit.SECONDS);
- }
-
- @Test
- public void itemViewSizeMatchParentEnforced() {
- mExpectedException.expect(IllegalStateException.class);
- mExpectedException.expectMessage(
- "Item's root view must fill the whole ViewPager2 (use match_parent)");
-
- ViewPager2 viewPager = new ViewPager2(InstrumentationRegistry.getContext());
- viewPager.setAdapter(new Adapter<ViewHolder>() {
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View view = new View(parent.getContext());
- view.setLayoutParams(new ViewGroup.LayoutParams(50, 50)); // arbitrary fixed size
- return new ViewHolder(view) {
- };
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- // do nothing
- }
-
- @Override
- public int getItemCount() {
- return 1;
- }
- });
-
- viewPager.measure(0, 0); // equivalent of unspecified
- }
-
- @Test
- public void childrenNotAllowed() throws Exception {
- mExpectedException.expect(IllegalStateException.class);
- mExpectedException.expectMessage("ViewPager2 does not support direct child views");
-
- Context context = InstrumentationRegistry.getContext();
- ViewPager2 viewPager = new ViewPager2(context);
- viewPager.addView(new View(context));
- }
-
- // TODO: verify correct padding behavior
- // TODO: add test for screen orientation change
- // TODO: port some of the fragment adapter tests as view adapter tests
-}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/BaseActivity.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/BaseActivity.java
new file mode 100644
index 0000000..5e3f326
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/BaseActivity.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget.swipe;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.testutils.RecreatedActivity;
+import androidx.viewpager2.test.R;
+import androidx.viewpager2.widget.ViewPager2;
+
+public abstract class BaseActivity extends RecreatedActivity {
+ private static final String ARG_TOTAL_PAGES = "totalPages";
+
+ protected ViewPager2 mViewPager;
+ protected int mTotalPages;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ setContentView(R.layout.activity_test_layout);
+
+ mViewPager = findViewById(R.id.view_pager);
+ mTotalPages = checkNotNull(getIntent().getExtras()).getInt(ARG_TOTAL_PAGES);
+
+ setAdapter();
+ }
+
+ protected abstract void setAdapter();
+
+ public abstract void validateState();
+
+ public abstract void updatePage(int pageIx, int newValue);
+
+ public static Intent createIntent(int totalPages) {
+ Intent intent = new Intent();
+ intent.putExtra(ARG_TOTAL_PAGES, totalPages);
+ return intent;
+ }
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/FragmentAdapterActivity.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/FragmentAdapterActivity.java
new file mode 100644
index 0000000..2d670a8
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/FragmentAdapterActivity.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget.swipe;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertThat;
+
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.widget.ViewPager2;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class FragmentAdapterActivity extends BaseActivity {
+ private static final Random RANDOM = new Random();
+
+ private AtomicInteger mAttachCount = new AtomicInteger(0);
+ private AtomicInteger mDestroyCount = new AtomicInteger(0);
+ private PageFragment[] mFragments;
+
+ @Override
+ protected void setAdapter() {
+ mFragments = new PageFragment[mTotalPages];
+
+ ViewPager2.FragmentProvider fragmentProvider = new ViewPager2.FragmentProvider() {
+ final boolean[] mWasEverAttached = new boolean[mTotalPages];
+
+ @Override
+ public Fragment getItem(final int position) {
+ PageFragment fragment = PageFragment.create(valueForPosition(position));
+
+ fragment.mOnAttachListener = new PageFragment.EventListener() {
+ @Override
+ public void onEvent(PageFragment fragment) {
+ mAttachCount.incrementAndGet();
+ mWasEverAttached[position] = true;
+ }
+ };
+
+ fragment.mOnDestroyListener = new PageFragment.EventListener() {
+ @Override
+ public void onEvent(PageFragment fragment) {
+ mDestroyCount.incrementAndGet();
+ }
+ };
+
+ return mFragments[position] = fragment;
+ }
+
+ private int valueForPosition(int position) {
+ // only supply correct value ones; then rely on it being kept by Fragment state
+ return mWasEverAttached[position]
+ ? RANDOM.nextInt() // junk value to be overridden by state saved value
+ : position;
+ }
+
+ @Override
+ public int getCount() {
+ return mTotalPages;
+ }
+ };
+
+ mViewPager.setAdapter(getSupportFragmentManager(), fragmentProvider,
+ ViewPager2.FragmentRetentionPolicy.SAVE_STATE);
+ }
+
+ @Override
+ public void updatePage(int pageIx, int newValue) {
+ mFragments[pageIx].updateValue(newValue);
+ }
+
+ @Override
+ public void validateState() {
+ assertThat(mAttachCount.get() - mDestroyCount.get(),
+ allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(4)));
+ }
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageFragment.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageFragment.java
new file mode 100644
index 0000000..23132bc
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageFragment.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget.swipe;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.test.R;
+
+public class PageFragment extends Fragment {
+ private static final String ARG_VALUE = "value";
+
+ private int mValue;
+ EventListener mOnAttachListener = EventListener.NO_OP;
+ EventListener mOnDestroyListener = EventListener.NO_OP;
+
+ static PageFragment create(int value) {
+ PageFragment result = new PageFragment();
+ Bundle args = new Bundle(1);
+ args.putInt(ARG_VALUE, value);
+ result.setArguments(args);
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.item_test_layout, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ Bundle data = savedInstanceState != null ? savedInstanceState : getArguments();
+ updateValue(data.getInt(ARG_VALUE));
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mOnAttachListener.onEvent(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ outState.putInt(ARG_VALUE, mValue);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mOnDestroyListener.onEvent(this);
+ }
+
+ void updateValue(int newValue) {
+ mValue = newValue;
+ ((TextView) getView()).setText(String.valueOf(newValue));
+ }
+
+ public interface EventListener {
+ void onEvent(PageFragment fragment);
+
+ EventListener NO_OP = new EventListener() {
+ @Override
+ public void onEvent(PageFragment fragment) {
+ // do nothing
+ }
+ };
+ }
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageSwiper.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageSwiper.java
new file mode 100644
index 0000000..a17ef94
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/PageSwiper.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget.swipe;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+import static androidx.core.util.Preconditions.checkArgumentNonnegative;
+import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
+
+import static org.hamcrest.CoreMatchers.allOf;
+
+import android.support.test.espresso.ViewAction;
+import android.support.test.espresso.action.ViewActions;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager2.test.R;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class PageSwiper {
+ private CountDownLatch mStableAfterSwipe;
+ private final int mLastPageIx;
+
+ public PageSwiper(int totalPages, RecyclerView recyclerView) {
+ mLastPageIx = checkArgumentNonnegative(totalPages - 1);
+
+ recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ // coming to idle from another state (dragging or setting) means we're stable now
+ if (newState == SCROLL_STATE_IDLE) {
+ mStableAfterSwipe.countDown();
+ }
+ }
+ });
+ }
+
+ public void swipe(int currentPageIx, int nextPageIx) throws InterruptedException {
+ if (nextPageIx > mLastPageIx) {
+ throw new IllegalArgumentException("Invalid next page: beyond last page.");
+ }
+
+ if (currentPageIx == nextPageIx) { // dedicated for testing edge behaviour
+ if (nextPageIx == 0) {
+ swipeRight(); // bounce off the left edge
+ return;
+ }
+ if (nextPageIx == mLastPageIx) { // bounce off the right edge
+ swipeLeft();
+ return;
+ }
+ throw new IllegalArgumentException(
+ "Invalid sequence. Not on an edge, and current page = next page.");
+ }
+
+ if (Math.abs(nextPageIx - currentPageIx) > 1) {
+ throw new IllegalArgumentException(
+ "Specified next page not adjacent to the current page.");
+ }
+
+ if (nextPageIx > currentPageIx) {
+ swipeLeft();
+ } else {
+ swipeRight();
+ }
+ }
+
+ private void swipeLeft() throws InterruptedException {
+ swipe(ViewActions.swipeLeft());
+ }
+
+ private void swipeRight() throws InterruptedException {
+ swipe(ViewActions.swipeRight());
+ }
+
+ private void swipe(ViewAction swipeAction) throws InterruptedException {
+ mStableAfterSwipe = new CountDownLatch(1);
+ onView(allOf(isDisplayed(), withId(R.id.text_view))).perform(swipeAction);
+ mStableAfterSwipe.await(1, TimeUnit.SECONDS);
+ }
+}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ViewAdapterActivity.java b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ViewAdapterActivity.java
new file mode 100644
index 0000000..8830561
--- /dev/null
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/ViewAdapterActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 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 androidx.viewpager2.widget.swipe;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.viewpager2.test.R;
+
+public class ViewAdapterActivity extends BaseActivity {
+ @Override
+ protected void setAdapter() {
+ mViewPager.setAdapter(new RecyclerView.Adapter<ViewHolder>() {
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
+ return new ViewHolder(
+ LayoutInflater.from(ViewAdapterActivity.this).inflate(
+ R.layout.item_test_layout, parent, false)) {
+ };
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ TextView view = (TextView) holder.itemView;
+ view.setText(String.valueOf(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mTotalPages;
+ }
+ });
+ }
+
+ @Override
+ public void validateState() {
+ // do nothing
+ }
+
+ @Override
+ public void updatePage(int pageIx, int newValue) {
+ throw new IllegalStateException("not implemented");
+ }
+}
diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
index 11c73f2..e5f1b3f 100644
--- a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
+++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java
@@ -455,11 +455,6 @@
getClass().getSimpleName() + " does not support direct child views");
}
- /** @see RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener) */
- public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
- mRecyclerView.addOnScrollListener(listener);
- }
-
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO(b/70666622): consider margin support